Merged PR 317: Add actions for application close, scan and fix. Rename software patch to fix

## Summary
Added actions for application close, scan and fix
Renamed software patch to software fix
Fixed and enabled `test_node_service_scan_integration`

## Test process
All tests passed locally

## Checklist
- [x] PR is linked to a **work item**
- [x] **acceptance criteria** of linked ticket are met
- [x] performed **self-review** of the code
- [x] 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: #2404
This commit is contained in:
Cristian Genes
2024-03-27 09:21:12 +00:00
23 changed files with 218 additions and 93 deletions

View File

@@ -48,7 +48,7 @@ class "ActiveNode" as primaite.nodes.active_node.ActiveNode {
file_system_state_actual : GOOD
file_system_state_observed : REPAIRING, RESTORING, GOOD
ip_address : str
patching_count : int
fixing_count : int
software_state
software_state : GOOD
set_file_system_state(file_system_state: FileSystemState) -> None
@@ -353,10 +353,10 @@ class "SB3Agent" as primaite.agents.sb3.SB3Agent {
}
class "Service" as primaite.common.service.Service {
name : str
patching_count : int
fixing_count : int
port : str
software_state : GOOD
reduce_patching_count() -> None
reduce_fixing_count() -> None
}
class "ServiceNode" as primaite.nodes.service_node.ServiceNode {
services : Dict[str, Service]
@@ -455,7 +455,7 @@ class "TrainingConfig" as primaite.config.training_config.TrainingConfig {
sb3_output_verbose_level
scanning : float
seed : Optional[int]
service_patching_duration : int
service_fixing_duration : int
session_type
time_delay : int
from_dict(config_dict: Dict[str, Any]) -> TrainingConfig

View File

@@ -41,11 +41,11 @@ The game layer is built on top of the simulator and it consumes the simulation a
* Hardware State (ON, OFF, RESETTING, SHUTTING_DOWN, BOOTING - enumeration)
Active Nodes also have the following attributes (Class: Active Node):
* IP Address
* Software State (GOOD, PATCHING, COMPROMISED - enumeration)
* Software State (GOOD, FIXING, COMPROMISED - enumeration)
* File System State (GOOD, CORRUPT, DESTROYED, REPAIRING, RESTORING - enumeration)
Service Nodes also have the following attributes (Class: Service Node):
* List of Services (where service is composed of service name and port). There is no theoretical limit on the number of services that can be modelled. Services and protocols are currently intrinsically linked (i.e. a service is an application on a node transmitting traffic of this protocol type)
* Service state (GOOD, PATCHING, COMPROMISED, OVERWHELMED - enumeration)
* Service state (GOOD, FIXING, COMPROMISED, OVERWHELMED - enumeration)
Passive Nodes are currently not used (but may be employed for non IP-based components such as machinery actuators in future releases).
**Links**
Links are modelled both as network edges (networkx) and as Python classes, in order to extend their functionality. Links include the following attributes:
@@ -70,8 +70,8 @@ The game layer is built on top of the simulator and it consumes the simulation a
* Running status (i.e. on / off)
The application of green agent IERs between a source and destination follows a number of rules. Specifically:
1. Does the current simulation time step fall between IER start and end step
2. Is the source node operational (both physically and at an O/S level), and is the service (protocol / port) associated with the IER (a) present on this node, and (b) in an operational state (i.e. not PATCHING)
3. Is the destination node operational (both physically and at an O/S level), and is the service (protocol / port) associated with the IER (a) present on this node, and (b) in an operational state (i.e. not PATCHING)
2. Is the source node operational (both physically and at an O/S level), and is the service (protocol / port) associated with the IER (a) present on this node, and (b) in an operational state (i.e. not FIXING)
3. Is the destination node operational (both physically and at an O/S level), and is the service (protocol / port) associated with the IER (a) present on this node, and (b) in an operational state (i.e. not FIXING)
4. Are there any Access Control List rules in place that prevent the application of this IER
5. Are all switches in the (OSPF) path between source and destination operational (both physically and at an O/S level)
For red agent IERs, the application of IERs between a source and destination follows a number of subtly different rules. Specifically:
@@ -95,7 +95,7 @@ The game layer is built on top of the simulator and it consumes the simulation a
* Active Nodes and Service Nodes:
* Software State:
* GOOD
* PATCHING - when a status of patching is entered, the node will automatically exit this state after a number of steps (as defined by the osPatchingDuration configuration item) after which it returns to a GOOD state
* FIXING - when a status of FIXING is entered, the node will automatically exit this state after a number of steps (as defined by the osFIXINGDuration configuration item) after which it returns to a GOOD state
* COMPROMISED
* File System State:
* GOOD
@@ -106,7 +106,7 @@ The game layer is built on top of the simulator and it consumes the simulation a
* Service Nodes only:
* Service State (for any associated service):
* GOOD
* PATCHING - when a status of patching is entered, the service will automatically exit this state after a number of steps (as defined by the servicePatchingDuration configuration item) after which it returns to a GOOD state
* FIXING - when a status of FIXING is entered, the service will automatically exit this state after a number of steps (as defined by the serviceFIXINGDuration configuration item) after which it returns to a GOOD state
* COMPROMISED
* OVERWHELMED
Red agent pattern-of-life has an additional feature not found in the green pattern-of-life. This is the ability to influence the state of the attributes of a node via a number of different conditions:
@@ -211,8 +211,8 @@ The game layer is built on top of the simulator and it consumes the simulation a
Hardware State (1=ON, 2=OFF, 3=RESETTING, 4=SHUTTING_DOWN, 5=BOOTING)
Operating System State (0=none, 1=GOOD, 2=PATCHING, 3=COMPROMISED)
File System State (0=none, 1=GOOD, 2=CORRUPT, 3=DESTROYED, 4=REPAIRING, 5=RESTORING)
Service1/Protocol1 state (0=none, 1=GOOD, 2=PATCHING, 3=COMPROMISED)
Service2/Protocol2 state (0=none, 1=GOOD, 2=PATCHING, 3=COMPROMISED)
Service1/Protocol1 state (0=none, 1=GOOD, 2=FIXING, 3=COMPROMISED)
Service2/Protocol2 state (0=none, 1=GOOD, 2=FIXING, 3=COMPROMISED)
]
(Note that each service available in the network is provided as a column, although not all nodes may utilise all services)
For the links, the following statuses are represented:
@@ -241,8 +241,8 @@ The game layer is built on top of the simulator and it consumes the simulation a
hardware_state (0=none, 1=ON, 2=OFF, 3=RESETTING, 4=SHUTTING_DOWN, 5=BOOTING)
software_state (0=none, 1=GOOD, 2=PATCHING, 3=COMPROMISED)
file_system_state (0=none, 1=GOOD, 2=CORRUPT, 3=DESTROYED, 4=REPAIRING, 5=RESTORING)
service1_state (0=none, 1=GOOD, 2=PATCHING, 3=COMPROMISED)
service2_state (0=none, 1=GOOD, 2=PATCHING, 3=COMPROMISED)
service1_state (0=none, 1=GOOD, 2=FIXING, 3=COMPROMISED)
service2_state (0=none, 1=GOOD, 2=FIXING, 3=COMPROMISED)
]
In a network with three nodes and two services, the full observation space would have 15 elements. It can be written with ``gym`` notation to indicate the number of discrete options for each of the elements of the observation space. For example:
.. code-block::
@@ -278,7 +278,7 @@ The game layer is built on top of the simulator and it consumes the simulation a
3. Any (Agent can take both node-based and ACL-based actions)
The choice of action space used during a training session is determined in the config_[name].yaml file.
**Node-Based**
The agent is able to influence the status of nodes by switching them off, resetting, or patching operating systems and services. In this instance, the action space is a Gymnasium spaces.Discrete type, as follows:
The agent is able to influence the status of nodes by switching them off, resetting, or FIXING operating systems and services. In this instance, the action space is a Gymnasium spaces.Discrete type, as follows:
* Dictionary item {... ,1: [x1, x2, x3,x4] ...}
The placeholders inside the list under the key '1' mean the following:
* [0, num nodes] - Node ID (0 = nothing, node ID)

View File

@@ -244,7 +244,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -339,7 +339,7 @@ agents:
folder_id: 0
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -246,7 +246,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -341,7 +341,7 @@ agents:
folder_id: 0
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0
@@ -797,7 +797,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -892,7 +892,7 @@ agents:
folder_id: 0
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -156,12 +156,12 @@ class NodeServiceEnableAction(NodeServiceAbstractAction):
self.verb: str = "enable"
class NodeServicePatchAction(NodeServiceAbstractAction):
"""Action which patches a service."""
class NodeServiceFixAction(NodeServiceAbstractAction):
"""Action which fixes a service."""
def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None:
super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services)
self.verb: str = "patch"
self.verb: str = "fix"
class NodeApplicationAbstractAction(AbstractAction):
@@ -195,6 +195,30 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction):
self.verb: str = "execute"
class NodeApplicationScanAction(NodeApplicationAbstractAction):
"""Action which scans an application."""
def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None:
super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications)
self.verb: str = "scan"
class NodeApplicationCloseAction(NodeApplicationAbstractAction):
"""Action which closes an application."""
def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None:
super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications)
self.verb: str = "close"
class NodeApplicationFixAction(NodeApplicationAbstractAction):
"""Action which fixes an application."""
def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None:
super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications)
self.verb: str = "fix"
class NodeFolderAbstractAction(AbstractAction):
"""
Base class for folder actions.
@@ -629,8 +653,11 @@ class ActionManager:
"NODE_SERVICE_RESTART": NodeServiceRestartAction,
"NODE_SERVICE_DISABLE": NodeServiceDisableAction,
"NODE_SERVICE_ENABLE": NodeServiceEnableAction,
"NODE_SERVICE_PATCH": NodeServicePatchAction,
"NODE_SERVICE_FIX": NodeServiceFixAction,
"NODE_APPLICATION_EXECUTE": NodeApplicationExecuteAction,
"NODE_APPLICATION_SCAN": NodeApplicationScanAction,
"NODE_APPLICATION_CLOSE": NodeApplicationCloseAction,
"NODE_APPLICATION_FIX": NodeApplicationFixAction,
"NODE_FILE_SCAN": NodeFileScanAction,
"NODE_FILE_CHECKHASH": NodeFileCheckhashAction,
"NODE_FILE_DELETE": NodeFileDeleteAction,

View File

@@ -208,7 +208,7 @@
"|--|--|\n",
"|0|UNUSED|\n",
"|1|GOOD|\n",
"|2|PATCHING|\n",
"|2|FIXING|\n",
"|3|COMPROMISED|\n",
"|4|OVERWHELMED|\n",
"\n",
@@ -520,7 +520,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The patching takes two steps, so the reward hasn't changed yet. Let's do nothing for another timestep, the reward should improve.\n",
"The fixing takes two steps, so the reward hasn't changed yet. Let's do nothing for another timestep, the reward should improve.\n",
"\n",
"The reward will increase slightly as soon as the file finishes restoring. Then, the reward will increase to 1 when both green agents make successful requests.\n",
"\n",

View File

@@ -3,6 +3,8 @@ from enum import Enum
from typing import Any, Dict, Set
from primaite import getLogger
from primaite.interface.request import RequestResponse
from primaite.simulator.core import RequestManager, RequestType
from primaite.simulator.system.software import IOSoftware, SoftwareHealthState
_LOGGER = getLogger(__name__)
@@ -38,6 +40,17 @@ class Application(IOSoftware):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def _init_request_manager(self) -> RequestManager:
"""
Initialise the request manager.
More information in user guide and docstring for SimComponent._init_request_manager.
"""
rm = super()._init_request_manager()
rm.add_request("close", RequestType(func=lambda request, context: RequestResponse.from_bool(self.close())))
return rm
@abstractmethod
def describe_state(self) -> Dict:
"""
@@ -104,11 +117,12 @@ class Application(IOSoftware):
"""The main application loop."""
pass
def close(self) -> None:
def close(self) -> bool:
"""Close the Application."""
if self.operating_state == ApplicationOperatingState.RUNNING:
self.sys_log.info(f"Closed Application{self.name}")
self.operating_state = ApplicationOperatingState.CLOSED
return True
def install(self) -> None:
"""Install Application."""

View File

@@ -304,8 +304,8 @@ class DatabaseService(Service):
self.backup_database()
return super().apply_timestep(timestep)
def _update_patch_status(self) -> None:
"""Perform a database restore when the patching countdown is finished."""
super()._update_patch_status()
if self._patching_countdown is None:
def _update_fix_status(self) -> None:
"""Perform a database restore when the FIXING countdown is finished."""
super()._update_fix_status()
if self._fixing_countdown is None:
self.restore_backup()

View File

@@ -43,8 +43,8 @@ class SoftwareHealthState(Enum):
"Unused state."
GOOD = 1
"The software is in a good and healthy condition."
PATCHING = 2
"The software is undergoing patching or updates."
FIXING = 2
"The software is undergoing FIXING or updates."
COMPROMISED = 3
"The software's security has been compromised."
OVERWHELMED = 4
@@ -82,7 +82,7 @@ class Software(SimComponent):
"The health state of the software visible to the red agent."
criticality: SoftwareCriticality = SoftwareCriticality.LOWEST
"The criticality level of the software."
patching_count: int = 0
fixing_count: int = 0
"The count of patches applied to the software, defaults to 0."
scanning_count: int = 0
"The count of times the software has been scanned, defaults to 0."
@@ -96,9 +96,9 @@ class Software(SimComponent):
"The FileSystem of the Node the Software is installed on."
folder: Optional[Folder] = None
"The folder on the file system the Software uses."
patching_duration: int = 2
fixing_duration: int = 2
"The number of ticks it takes to patch the software."
_patching_countdown: Optional[int] = None
_fixing_countdown: Optional[int] = None
"Current number of ticks left to patch the software."
def _init_request_manager(self) -> RequestManager:
@@ -117,9 +117,9 @@ class Software(SimComponent):
),
)
rm.add_request(
"patch",
"fix",
RequestType(
func=lambda request, context: RequestResponse.from_bool(self.patch()),
func=lambda request, context: RequestResponse.from_bool(self.fix()),
),
)
rm.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan())))
@@ -149,7 +149,7 @@ class Software(SimComponent):
"health_state_actual": self.health_state_actual.value,
"health_state_visible": self.health_state_visible.value,
"criticality": self.criticality.value,
"patching_count": self.patching_count,
"fixing_count": self.fixing_count,
"scanning_count": self.scanning_count,
"revealed_to_red": self.revealed_to_red,
}
@@ -194,21 +194,21 @@ class Software(SimComponent):
self.health_state_visible = self.health_state_actual
return True
def patch(self) -> bool:
"""Perform a patch on the software."""
def fix(self) -> bool:
"""Perform a fix on the software."""
if self.health_state_actual in (SoftwareHealthState.COMPROMISED, SoftwareHealthState.GOOD):
self._patching_countdown = self.patching_duration
self.set_health_state(SoftwareHealthState.PATCHING)
self._fixing_countdown = self.fixing_duration
self.set_health_state(SoftwareHealthState.FIXING)
return True
return False
def _update_patch_status(self) -> None:
"""Update the patch status of the software."""
self._patching_countdown -= 1
if self._patching_countdown <= 0:
def _update_fix_status(self) -> None:
"""Update the fix status of the software."""
self._fixing_countdown -= 1
if self._fixing_countdown <= 0:
self.set_health_state(SoftwareHealthState.GOOD)
self._patching_countdown = None
self.patching_count += 1
self._fixing_countdown = None
self.fixing_count += 1
def reveal_to_red(self) -> None:
"""Reveals the software to the red agent."""
@@ -221,8 +221,8 @@ class Software(SimComponent):
:param timestep: The current timestep of the simulation.
"""
super().apply_timestep(timestep)
if self.health_state_actual == SoftwareHealthState.PATCHING:
self._update_patch_status()
if self.health_state_actual == SoftwareHealthState.FIXING:
self._update_fix_status()
class IOSoftware(Software):

View File

@@ -155,7 +155,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -250,7 +250,7 @@ agents:
folder_id: 1
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -159,7 +159,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -254,7 +254,7 @@ agents:
folder_id: 1
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -166,7 +166,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -261,7 +261,7 @@ agents:
folder_id: 1
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0
@@ -610,7 +610,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -705,7 +705,7 @@ agents:
folder_id: 1
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -244,7 +244,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -339,7 +339,7 @@ agents:
folder_id: 0
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -169,7 +169,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -264,7 +264,7 @@ agents:
folder_id: 1
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

View File

@@ -167,7 +167,7 @@ agents:
- type: NODE_SERVICE_RESTART
- type: NODE_SERVICE_DISABLE
- type: NODE_SERVICE_ENABLE
- type: NODE_SERVICE_PATCH
- type: NODE_SERVICE_FIX
- type: NODE_FILE_SCAN
- type: NODE_FILE_CHECKHASH
- type: NODE_FILE_DELETE
@@ -262,7 +262,7 @@ agents:
folder_id: 1
file_id: 0
13:
action: "NODE_SERVICE_PATCH"
action: "NODE_SERVICE_FIX"
options:
node_id: 2
service_id: 0

File diff suppressed because one or more lines are too long

View File

@@ -475,8 +475,11 @@ def game_and_agent():
{"type": "NODE_SERVICE_RESTART"},
{"type": "NODE_SERVICE_DISABLE"},
{"type": "NODE_SERVICE_ENABLE"},
{"type": "NODE_SERVICE_PATCH"},
{"type": "NODE_SERVICE_FIX"},
{"type": "NODE_APPLICATION_EXECUTE"},
{"type": "NODE_APPLICATION_SCAN"},
{"type": "NODE_APPLICATION_CLOSE"},
{"type": "NODE_APPLICATION_FIX"},
{"type": "NODE_FILE_SCAN"},
{"type": "NODE_FILE_CHECKHASH"},
{"type": "NODE_FILE_DELETE"},

View File

@@ -17,6 +17,7 @@ import pytest
from primaite.game.agent.interface import ProxyAgent
from primaite.game.game import PrimaiteGame
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
from primaite.simulator.system.applications.application import ApplicationOperatingState
from primaite.simulator.system.applications.web_browser import WebBrowser
from primaite.simulator.system.software import SoftwareHealthState
@@ -30,7 +31,6 @@ def test_do_nothing_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent])
game.step()
@pytest.mark.skip(reason="Waiting to merge ticket 2166")
def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]):
"""
Test that the NodeServiceScanAction can form a request and that it is accepted by the simulation.
@@ -42,12 +42,12 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy
game, agent = game_and_agent
# 1: Check that the service starts off in a good state, and that visible state is hidden until first scan
svc = game.simulation.network.get_node_by_hostname("client_1").software_manager.software.get("DNSClient")
svc = game.simulation.network.get_node_by_hostname("server_1").software_manager.software.get("DNSServer")
assert svc.health_state_actual == SoftwareHealthState.GOOD
assert svc.health_state_visible == SoftwareHealthState.UNUSED
# 2: Scan and check that the visible state is now correct
action = ("NODE_SERVICE_SCAN", {"node_id": 0, "service_id": 0})
action = ("NODE_SERVICE_SCAN", {"node_id": 1, "service_id": 0})
agent.store_action(action)
game.step()
assert svc.health_state_actual == SoftwareHealthState.GOOD
@@ -58,18 +58,18 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy
assert svc.health_state_visible == SoftwareHealthState.GOOD
# 4: Scan and check that the visible state is now correct
action = ("NODE_SERVICE_SCAN", {"node_id": 0, "service_id": 0})
action = ("NODE_SERVICE_SCAN", {"node_id": 1, "service_id": 0})
agent.store_action(action)
game.step()
assert svc.health_state_actual == SoftwareHealthState.COMPROMISED
assert svc.health_state_visible == SoftwareHealthState.COMPROMISED
def test_node_service_patch_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]):
def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]):
"""
Test that the NodeServicePatchAction can form a request and that it is accepted by the simulation.
Test that the NodeServiceFixAction can form a request and that it is accepted by the simulation.
When you initiate a patch action, the software health state turns to PATCHING, then after a few steps, it goes
When you initiate a patch action, the software health state turns to FIXING, then after a few steps, it goes
to GOOD.
"""
game, agent = game_and_agent
@@ -79,12 +79,12 @@ def test_node_service_patch_integration(game_and_agent: Tuple[PrimaiteGame, Prox
svc.health_state_actual = SoftwareHealthState.COMPROMISED
# 2: Apply a patch action
action = ("NODE_SERVICE_PATCH", {"node_id": 1, "service_id": 0})
action = ("NODE_SERVICE_FIX", {"node_id": 1, "service_id": 0})
agent.store_action(action)
game.step()
# 3: Check that the service is now in the patching state
assert svc.health_state_actual == SoftwareHealthState.PATCHING
# 3: Check that the service is now in the FIXING state
assert svc.health_state_actual == SoftwareHealthState.FIXING
# 4: perform a few do-nothing steps and check that the service is now in the good state
action = ("DONOTHING", {})
@@ -374,3 +374,84 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa
# 3: Check that the Port is enabled, and that client 1 can ping again
assert router.network_interface[1].enabled == True
assert client_1.ping("10.0.2.3")
def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]):
"""Test that the NodeApplicationScanAction updates the application status as expected."""
game, agent = game_and_agent
# 1: Check that http traffic is going across the network nicely.
client_1 = game.simulation.network.get_node_by_hostname("client_1")
browser: WebBrowser = client_1.software_manager.software.get("WebBrowser")
browser.run()
browser.target_url = "http://www.example.com"
assert browser.get_webpage() # check that the browser can access example.com
assert browser.health_state_actual == SoftwareHealthState.GOOD
assert browser.health_state_visible == SoftwareHealthState.UNUSED
# 2: Scan and check that the visible state is now correct
action = ("NODE_APPLICATION_SCAN", {"node_id": 0, "application_id": 0})
agent.store_action(action)
game.step()
assert browser.health_state_actual == SoftwareHealthState.GOOD
assert browser.health_state_visible == SoftwareHealthState.GOOD
# 3: Corrupt the service and check that the visible state is still good
browser.health_state_actual = SoftwareHealthState.COMPROMISED
assert browser.health_state_visible == SoftwareHealthState.GOOD
# 4: Scan and check that the visible state is now correct
action = ("NODE_APPLICATION_SCAN", {"node_id": 0, "application_id": 0})
agent.store_action(action)
game.step()
assert browser.health_state_actual == SoftwareHealthState.COMPROMISED
assert browser.health_state_visible == SoftwareHealthState.COMPROMISED
def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]):
"""Test that the NodeApplicationFixAction can form a request and that it is accepted by the simulation.
When you initiate a fix action, the software health state turns to FIXING, then after a few steps, it goes
to GOOD."""
game, agent = game_and_agent
# 1: Check that http traffic is going across the network nicely.
client_1 = game.simulation.network.get_node_by_hostname("client_1")
browser: WebBrowser = client_1.software_manager.software.get("WebBrowser")
browser.health_state_actual = SoftwareHealthState.COMPROMISED
# 2: Apply a fix action
action = ("NODE_APPLICATION_FIX", {"node_id": 0, "application_id": 0})
agent.store_action(action)
game.step()
# 3: Check that the application is now in the FIXING state
assert browser.health_state_actual == SoftwareHealthState.FIXING
# 4: perform a few do-nothing steps and check that the application is now in the good state
action = ("DONOTHING", {})
agent.store_action(action)
game.step()
assert browser.health_state_actual == SoftwareHealthState.GOOD
def test_node_application_close_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]):
"""Test that the NodeApplicationCloseAction can form a request and that it is accepted by the simulation.
When you initiate a close action, the Application Operating State changes for CLOSED."""
game, agent = game_and_agent
client_1 = game.simulation.network.get_node_by_hostname("client_1")
browser: WebBrowser = client_1.software_manager.software.get("WebBrowser")
browser.run()
assert browser.operating_state == ApplicationOperatingState.RUNNING
# 2: Apply a close action
action = ("NODE_APPLICATION_CLOSE", {"node_id": 0, "application_id": 0})
agent.store_action(action)
game.step()
assert browser.operating_state == ApplicationOperatingState.CLOSED

View File

@@ -23,7 +23,7 @@ def test_successful_application_requests(example_network):
resp_1 = net.apply_request(["node", "client_1", "application", "TestApplication", "scan"])
assert resp_1 == RequestResponse(status="success", data={})
resp_2 = net.apply_request(["node", "client_1", "application", "TestApplication", "patch"])
resp_2 = net.apply_request(["node", "client_1", "application", "TestApplication", "fix"])
assert resp_2 == RequestResponse(status="success", data={})
resp_3 = net.apply_request(["node", "client_1", "application", "TestApplication", "compromise"])
assert resp_3 == RequestResponse(status="success", data={})
@@ -46,7 +46,7 @@ def test_successful_service_requests(example_network):
"resume",
"compromise",
"scan",
"patch",
"fix",
]:
resp_1 = net.apply_request(["node", "server_1", "service", "TestService", verb])
assert resp_1 == RequestResponse(status="success", data={})

View File

@@ -46,5 +46,5 @@ def test_application_describe_states(application):
application.set_health_state(SoftwareHealthState.COMPROMISED)
assert SoftwareHealthState.COMPROMISED.value == application.describe_state().get("health_state_actual")
application.patch()
assert SoftwareHealthState.PATCHING.value == application.describe_state().get("health_state_actual")
application.fix()
assert SoftwareHealthState.FIXING.value == application.describe_state().get("health_state_actual")

View File

@@ -104,7 +104,7 @@ def test_query_fail_to_connect(database_client_on_computer):
"""Database client query should return False if the connect attempt fails."""
database_client, computer = database_client_on_computer
def return_false():
def return_false(**kwargs):
return False
database_client.connect = return_false

View File

@@ -80,14 +80,14 @@ def test_service_enable(service):
assert service.operating_state == ServiceOperatingState.STOPPED
def test_service_patch(service):
"""Test that a service can be patched and that it takes two timesteps to complete."""
def test_service_fix(service):
"""Test that a service can be fixed and that it takes two timesteps to complete."""
service.start()
assert service.health_state_actual == SoftwareHealthState.GOOD
service.apply_request(["patch"])
assert service.health_state_actual == SoftwareHealthState.PATCHING
service.apply_request(["fix"])
assert service.health_state_actual == SoftwareHealthState.FIXING
service.apply_timestep(1)
assert service.health_state_actual == SoftwareHealthState.PATCHING
assert service.health_state_actual == SoftwareHealthState.FIXING
service.apply_timestep(2)
assert service.health_state_actual == SoftwareHealthState.GOOD

View File

@@ -93,7 +93,7 @@ def test_restart_compromised(service):
"""
Service should be compromised even after reset.
Only way to remove compromised status is via patching.
Only way to remove compromised status is via FIXING.
"""
timestep = 0
@@ -136,16 +136,16 @@ def test_compromised_service_remains_compromised(service):
assert service.health_state_actual == SoftwareHealthState.COMPROMISED
def test_service_patching(service):
def test_service_fixing(service):
service.start()
assert service.health_state_actual == SoftwareHealthState.GOOD
service.set_health_state(SoftwareHealthState.COMPROMISED)
service.patch()
assert service.health_state_actual == SoftwareHealthState.PATCHING
service.fix()
assert service.health_state_actual == SoftwareHealthState.FIXING
for i in range(service.patching_duration + 1):
for i in range(service.fixing_duration + 1):
service.apply_timestep(i)
assert service.health_state_actual == SoftwareHealthState.GOOD