From 8783574442e24d31813f208dff94b4431eb21a72 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Fri, 27 Oct 2023 10:17:59 +0100 Subject: [PATCH 1/4] #1961: os scan set up --- .../simulator/file_system/file_system.py | 5 +++ .../simulator/network/hardware/base.py | 34 ++++++++++++++++++- .../simulator/system/services/service.py | 5 --- .../_network/_hardware/test_node_actions.py | 31 +++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 55af71c4..cddd276a 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -231,6 +231,11 @@ class FileSystem(SimComponent): state["folders"] = {folder.name: folder.describe_state() for folder in self.folders.values()} return state + def scan(self): + """Scan all the folders and files in the file system.""" + for folder_id in self.folders: + self.folders[folder_id].scan() + def create_folder(self, folder_name: str) -> Folder: """ Creates a Folder and adds it to the list of folders. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 7099e4c7..153a0a15 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: ...)) # TODO implement OS scan + rm.add_request("scan", RequestType(func=lambda request, context: self.scan(reveal_to_red=True))) rm.add_request("shutdown", RequestType(func=lambda request, context: self.power_off())) rm.add_request("startup", RequestType(func=lambda request, context: self.power_on())) @@ -986,6 +986,10 @@ class Node(SimComponent): rm.add_request("logon", RequestType(func=lambda request, context: ...)) # TODO implement logon request rm.add_request("logoff", RequestType(func=lambda request, context: ...)) # TODO implement logoff request + self._os_request_manager = RequestManager() + self._os_request_manager.add_request("scan", RequestType(func=lambda request, context: self.scan())) + rm.add_request("os", RequestType(func=self._os_request_manager)) + return rm def _install_system_software(self): @@ -1086,6 +1090,34 @@ class Node(SimComponent): self.operating_state = NodeOperatingState.OFF self.sys_log.info("Turned off") + def scan(self): + """ + Scan the node and all the items within it. + + Scans the: + - Processes + - Services + - Applications + - Folders + - Files + + to the red agent. + """ + # scan processes + for process_id in self.processes: + self.processes[process_id].scan() + + # scan services + for service_id in self.services: + self.services[service_id].scan() + + # scan applications + for application_id in self.applications: + self.applications[application_id].scan() + + # scan file system + self.file_system.scan() + def power_on(self): """Power on the Node, enabling its NICs if it is in the OFF state.""" if self.operating_state == NodeOperatingState.OFF: diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 24de027c..079f1dcf 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -82,11 +82,6 @@ class Service(IOSoftware): """ pass - def scan(self) -> None: - """Update the service visible states.""" - # update the visible operating state - self.health_state_visible = self.health_state_actual - def stop(self) -> None: """Stop the service.""" if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]: 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 e03e1d28..de4e0745 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,9 @@ import pytest from primaite.simulator.network.hardware.base import Node, NodeOperatingState +from primaite.simulator.system.processes.process import Process +from primaite.simulator.system.services.service import Service +from primaite.simulator.system.software import SoftwareHealthState @pytest.fixture @@ -39,3 +42,31 @@ def test_node_shutdown(node): idx += 1 assert node.operating_state == NodeOperatingState.OFF + + +def test_node_os_scan(node): + """Test OS Scanning.""" + # 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 + + # add services to node + service = Service(name="service") + service.health_state_actual = SoftwareHealthState.COMPROMISED + node.install_service(service=service) + + # add application to node + + # add file to node + + # run os scan + node.apply_request(["os", "scan"]) + + # apply time steps + for i in range(20): + node.apply_timestep(timestep=i) + + # should update the state of all items + assert node.processes["process"].health_state_visible == SoftwareHealthState.COMPROMISED + assert service.health_state_visible == SoftwareHealthState.COMPROMISED From 68b22b6444bae2f46a172e4affe9083992960b5b Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Fri, 27 Oct 2023 17:50:41 +0100 Subject: [PATCH 2/4] #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 From 1ddf400d6f37b903c031607c9f83ad1f56957f31 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Fri, 27 Oct 2023 18:28:34 +0100 Subject: [PATCH 3/4] #1961: node resetting --- .../simulator/network/hardware/base.py | 24 +++++++++++++++++- .../_network/_hardware/test_node_actions.py | 25 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 1858345b..945eb345 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -926,6 +926,9 @@ class Node(SimComponent): shut_down_countdown: int = 0 "Time steps needed until node is shut down." + is_resetting: bool = False + "If true, the node will try turning itself off then back on again." + def __init__(self, **kwargs): """ Initialize the Node with various components and managers. @@ -982,7 +985,7 @@ class Node(SimComponent): rm.add_request("shutdown", RequestType(func=lambda request, context: self.power_off())) rm.add_request("startup", RequestType(func=lambda request, context: self.power_on())) - rm.add_request("reset", RequestType(func=lambda request, context: ...)) # TODO implement node reset + rm.add_request("reset", RequestType(func=lambda request, context: self.reset())) # TODO implement node reset rm.add_request("logon", RequestType(func=lambda request, context: ...)) # TODO implement logon request rm.add_request("logoff", RequestType(func=lambda request, context: ...)) # TODO implement logoff request @@ -1090,6 +1093,11 @@ class Node(SimComponent): self.operating_state = NodeOperatingState.OFF self.sys_log.info("Turned off") + # if resetting turn back on + if self.is_resetting: + self.is_resetting = False + self.power_on() + # apply time step to node components if self.operating_state == NodeOperatingState.ON: for process_id in self.processes: @@ -1184,6 +1192,20 @@ class Node(SimComponent): self.operating_state = NodeOperatingState.OFF self.sys_log.info("Turned off") + def reset(self): + """ + Resets the node. + + Powers off the node and sets is_resetting to True. + Applying more timesteps will eventually turn the node back on. + """ + if not self.operating_state.ON: + self.sys_log.error(f"Cannot reset {self.hostname} - node is not turned on.") + else: + self.is_resetting = True + self.sys_log.info(f"Resetting {self.hostname}...") + self.power_off() + def connect_nic(self, nic: NIC): """ Connect a NIC (Network Interface Card) to the node. 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 f886b8fe..6161bbf6 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 @@ -123,3 +123,28 @@ def test_node_red_scan(node, service, application): assert application.revealed_to_red is True assert folder.revealed_to_red is True assert file.revealed_to_red is True + + +def test_reset_node(node): + """Test that a node can be reset.""" + node.operating_state = NodeOperatingState.ON + + node.apply_request(["reset"]) + assert node.operating_state == NodeOperatingState.SHUTTING_DOWN + + """ + 3 steps to shut down + 2 steps to set up the turning of it back on + 3 steps to turn back on + + 3 + 2 + 3 = 8 + kwik mafs + """ + + for i in range(8): + node.apply_timestep(timestep=i) + + if i == 3: + assert node.operating_state == NodeOperatingState.BOOTING + + assert node.operating_state == NodeOperatingState.ON From 98ca33e9949e5d47fc3f9564a10ea7dd47a7cac0 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Mon, 30 Oct 2023 15:34:13 +0000 Subject: [PATCH 4/4] #1961: scanning no longer happens every timestep - the scan is all done in one timestep after the required timestep countdown is complete --- .../simulator/file_system/file_system.py | 87 +++++++++++++------ .../simulator/network/hardware/base.py | 80 ++++++++++------- .../_file_system/test_file_system.py | 35 ++++++-- .../_file_system/test_file_system_actions.py | 11 +-- .../_network/_hardware/test_node_actions.py | 10 ++- 5 files changed, 150 insertions(+), 73 deletions(-) diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index af76254d..4ffe6a6d 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -98,6 +98,7 @@ class FileSystemItemABC(SimComponent): state["status"] = self.health_status.name state["visible_status"] = self.visible_health_status.name state["previous_hash"] = self.previous_hash + state["revealed_to_red"] = self.revealed_to_red return state def _init_request_manager(self) -> RequestManager: @@ -124,10 +125,6 @@ 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: """ @@ -246,15 +243,23 @@ class FileSystem(SimComponent): for folder_id in self.folders: self.folders[folder_id].apply_timestep(timestep=timestep) - def scan(self): - """Scan all the folders (and child files) in the file system.""" - for folder_id in self.folders: - self.folders[folder_id].scan() + def scan(self, instant_scan: bool = False): + """ + Scan all the folders (and child files) in the file system. - def reveal_to_red(self): - """Reveals all the folders (and child files) in the file system to the red agent.""" + :param: instant_scan: If True, the scan is completed instantly and ignores scan duration. Default False. + """ for folder_id in self.folders: - self.folders[folder_id].reveal_to_red() + self.folders[folder_id].scan(instant_scan=instant_scan) + + def reveal_to_red(self, instant_scan: bool = False): + """ + Reveals all the folders (and child files) in the file system to the red agent. + + :param: instant_scan: If True, the scan is completed instantly and ignores scan duration. Default False. + """ + for folder_id in self.folders: + self.folders[folder_id].reveal_to_red(instant_scan=instant_scan) def create_folder(self, folder_name: str) -> Folder: """ @@ -529,20 +534,25 @@ class Folder(FileSystemItemABC): # scan files each timestep if self.scan_duration >= 0: - # scan one file per timestep - 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 + if self.scan_duration == 0: + for file_id in self.files: + file = self.get_file_by_id(file_uuid=file_id) + file.scan() + if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: + self.visible_health_status = FileSystemItemHealthStatus.CORRUPT + # 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 + if self.red_scan_duration == 0: + self.revealed_to_red = True + for file_id in self.files: + file = self.get_file_by_id(file_uuid=file_id) + file.reveal_to_red() + # apply timestep to files in folder for file_id in self.files: self.files[file_id].apply_timestep(timestep=timestep) @@ -641,23 +651,44 @@ class Folder(FileSystemItemABC): """Returns true if the folder is being quarantined.""" pass - def scan(self) -> None: - """Update Folder visible status.""" + def scan(self, instant_scan: bool = False) -> None: + """ + Update Folder visible status. + + :param: instant_scan: If True, the scan is completed instantly and ignores scan duration. Default False. + """ + if instant_scan: + for file_id in self.files: + file = self.get_file_by_id(file_uuid=file_id) + file.scan() + if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: + self.visible_health_status = FileSystemItemHealthStatus.CORRUPT + return + if self.scan_duration <= 0: # scan one file per timestep - self.scan_duration = len(self.files) - 1 + self.scan_duration = len(self.files) 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() + def reveal_to_red(self, instant_scan: bool = False): + """ + Reveals the folders and files to the red agent. + + :param: instant_scan: If True, the scan is completed instantly and ignores scan duration. Default False. + """ + if instant_scan: + self.revealed_to_red = True + for file_id in self.files: + file = self.get_file_by_id(file_uuid=file_id) + file.reveal_to_red() + return if self.red_scan_duration <= 0: # scan one file per timestep - self.red_scan_duration = len(self.files) - 1 + self.red_scan_duration = len(self.files) self.fs.sys_log.info(f"Folder revealed to red agent: {self.name} (id: {self.uuid})") else: # scan already in progress @@ -830,6 +861,10 @@ class File(FileSystemItemABC): self.folder.fs.sys_log.info(f"Scanning file {self.sim_path if self.sim_path else path}") self.visible_health_status = self.health_status + def reveal_to_red(self): + """Reveals the folder/file to the red agent.""" + self.revealed_to_red = True + def check_hash(self) -> bool: """ Check if the file has been changed. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 945eb345..9dc1a2ff 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -929,6 +929,15 @@ class Node(SimComponent): is_resetting: bool = False "If true, the node will try turning itself off then back on again." + node_scan_duration: int = 10 + "How many timesteps until the whole node is scanned. Default 10 time steps." + + node_scan_countdown: int = 0 + "Time steps until scan is complete" + + red_scan_countdown: int = 0 + "Time steps until reveal to red scan is complete." + def __init__(self, **kwargs): """ Initialize the Node with various components and managers. @@ -1098,8 +1107,47 @@ class Node(SimComponent): self.is_resetting = False self.power_on() - # apply time step to node components + # time steps which require the node to be on if self.operating_state == NodeOperatingState.ON: + # node scanning + if self.node_scan_countdown > 0: + self.node_scan_countdown -= 1 + + if self.node_scan_countdown == 0: + # scan everything! + for process_id in self.processes: + self.processes[process_id].scan() + + # scan services + for service_id in self.services: + self.services[service_id].scan() + + # scan applications + for application_id in self.applications: + self.applications[application_id].scan() + + # scan file system + self.file_system.scan(instant_scan=True) + + if self.red_scan_countdown > 0: + self.red_scan_countdown -= 1 + + if self.red_scan_countdown == 0: + # 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(instant_scan=True) + for process_id in self.processes: self.processes[process_id].apply_timestep(timestep=timestep) @@ -1124,20 +1172,7 @@ class Node(SimComponent): to the red agent. """ - # scan processes - for process_id in self.processes: - self.processes[process_id].scan() - - # scan services - for service_id in self.services: - self.services[service_id].scan() - - # scan applications - for application_id in self.applications: - self.applications[application_id].scan() - - # scan file system - self.file_system.scan() + self.node_scan_countdown = self.node_scan_duration def reveal_to_red(self) -> None: """ @@ -1152,20 +1187,7 @@ class Node(SimComponent): `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() + self.red_scan_countdown = self.node_scan_duration def power_on(self): """Power on the Node, enabling its NICs if it is in the OFF state.""" diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py index 2404f30d..12d9b94c 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py @@ -215,8 +215,8 @@ def test_folder_scan(file_system): folder.apply_timestep(timestep=0) assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT - assert file1.visible_health_status == FileSystemItemHealthStatus.CORRUPT + assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD folder.apply_timestep(timestep=1) @@ -226,12 +226,33 @@ def test_folder_scan(file_system): assert file1.visible_health_status == FileSystemItemHealthStatus.CORRUPT assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT - folder.apply_timestep(timestep=2) - assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT - assert file1.visible_health_status == FileSystemItemHealthStatus.CORRUPT - assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT +def test_folder_reveal_to_red_scan(file_system): + """Test the ability to reveal files to red.""" + folder: Folder = file_system.create_folder(folder_name="test_folder") + file_system.create_file(file_name="test_file.txt", folder_name="test_folder") + file_system.create_file(file_name="test_file2.txt", folder_name="test_folder") + + file1: File = folder.get_file_by_id(file_uuid=list(folder.files)[1]) + file2: File = folder.get_file_by_id(file_uuid=list(folder.files)[0]) + + assert folder.revealed_to_red is False + assert file1.revealed_to_red is False + assert file2.revealed_to_red is False + + folder.reveal_to_red() + + folder.apply_timestep(timestep=0) + + assert folder.revealed_to_red is False + assert file1.revealed_to_red is False + assert file2.revealed_to_red is False + + folder.apply_timestep(timestep=1) + + assert folder.revealed_to_red is True + assert file1.revealed_to_red is True + assert file2.revealed_to_red is True def test_simulated_file_check_hash(file_system): diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py index 23115fd7..abfb244a 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py @@ -47,8 +47,8 @@ def test_folder_scan_request(populated_file_system): folder.apply_timestep(timestep=0) assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT - assert file1.visible_health_status == FileSystemItemHealthStatus.CORRUPT + assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD folder.apply_timestep(timestep=1) @@ -58,13 +58,6 @@ def test_folder_scan_request(populated_file_system): assert file1.visible_health_status == FileSystemItemHealthStatus.CORRUPT assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT - folder.apply_timestep(timestep=2) - - assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT - assert file1.visible_health_status == FileSystemItemHealthStatus.CORRUPT - assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT - def test_file_checkhash_request(populated_file_system): """Test that an agent can request a file hash check.""" 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 6161bbf6..3d6eea3b 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 @@ -69,14 +69,16 @@ def test_node_os_scan(node, service, application): assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD file: File = node.file_system.create_file(folder_name="test_folder", file_name="file.txt") + file2: File = node.file_system.create_file(folder_name="test_folder", file_name="file2.txt") file.corrupt() + file2.corrupt() assert file.visible_health_status == FileSystemItemHealthStatus.GOOD # run os scan node.apply_request(["os", "scan"]) # apply time steps - for i in range(20): + for i in range(10): node.apply_timestep(timestep=i) # should update the state of all items @@ -85,6 +87,7 @@ def test_node_os_scan(node, service, application): assert application.health_state_visible == SoftwareHealthState.COMPROMISED assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT + assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT def test_node_red_scan(node, service, application): @@ -108,13 +111,15 @@ def test_node_red_scan(node, service, application): assert folder.revealed_to_red is False file: File = node.file_system.create_file(folder_name="test_folder", file_name="file.txt") + file2: File = node.file_system.create_file(folder_name="test_folder", file_name="file2.txt") assert file.revealed_to_red is False + assert file2.revealed_to_red is False # run os scan node.apply_request(["scan"]) # apply time steps - for i in range(20): + for i in range(10): node.apply_timestep(timestep=i) # should update the state of all items @@ -123,6 +128,7 @@ def test_node_red_scan(node, service, application): assert application.revealed_to_red is True assert folder.revealed_to_red is True assert file.revealed_to_red is True + assert file2.revealed_to_red is True def test_reset_node(node):