#1961: node scanning + applying timestep to all components within node + node revealing to red

This commit is contained in:
Czar.Echavez
2023-10-27 17:50:41 +01:00
parent 8783574442
commit 68b22b6444
6 changed files with 216 additions and 18 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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