Backport 3.3.1 fixes into Core

This commit is contained in:
Marek Wolan
2025-01-21 13:08:36 +00:00
parent 4b79c88ae5
commit 66daab3baf
57 changed files with 441 additions and 247 deletions

View File

@@ -14,31 +14,36 @@ parameters:
- name: matrix - name: matrix
type: object type: object
default: default:
# - job_name: 'UbuntuPython38' - job_name: 'UbuntuPython39'
# py: '3.8' py: '3.9'
# img: 'ubuntu-latest' img: 'ubuntu-latest'
# every_time: false every_time: false
# publish_coverage: false publish_coverage: false
- job_name: 'UbuntuPython311' - job_name: 'UbuntuPython310'
py: '3.11' py: '3.10'
img: 'ubuntu-latest' img: 'ubuntu-latest'
every_time: true every_time: true
publish_coverage: true publish_coverage: true
# - job_name: 'WindowsPython38' - job_name: 'UbuntuPython311'
# py: '3.8' py: '3.11'
# img: 'windows-latest' img: 'ubuntu-latest'
# every_time: false every_time: false
# publish_coverage: false publish_coverage: false
- job_name: 'WindowsPython39'
py: '3.9'
img: 'windows-latest'
every_time: false
publish_coverage: false
- job_name: 'WindowsPython311' - job_name: 'WindowsPython311'
py: '3.11' py: '3.11'
img: 'windows-latest' img: 'windows-latest'
every_time: false every_time: false
publish_coverage: false publish_coverage: false
# - job_name: 'MacOSPython38' - job_name: 'MacOSPython39'
# py: '3.8' py: '3.9'
# img: 'macOS-latest' img: 'macOS-latest'
# every_time: false every_time: false
# publish_coverage: false publish_coverage: false
- job_name: 'MacOSPython311' - job_name: 'MacOSPython311'
py: '3.11' py: '3.11'
img: 'macOS-latest' img: 'macOS-latest'
@@ -63,7 +68,7 @@ stages:
displayName: 'Use Python ${{ item.py }}' displayName: 'Use Python ${{ item.py }}'
- script: | - script: |
python -m pip install pre-commit python -m pip install pre-commit>=6.1
pre-commit install pre-commit install
pre-commit run --all-files pre-commit run --all-files
displayName: 'Run pre-commits' displayName: 'Run pre-commits'
@@ -71,7 +76,6 @@ stages:
- script: | - script: |
python -m pip install --upgrade pip==23.0.1 python -m pip install --upgrade pip==23.0.1
pip install wheel==0.38.4 --upgrade pip install wheel==0.38.4 --upgrade
pip install setuptools==66 --upgrade
pip install build==0.10.0 pip install build==0.10.0
pip install pytest-azurepipelines pip install pytest-azurepipelines
displayName: 'Install build dependencies' displayName: 'Install build dependencies'

View File

@@ -31,7 +31,7 @@ repos:
- id: isort - id: isort
args: [ "--profile", "black" ] args: [ "--profile", "black" ]
- repo: http://github.com/PyCQA/flake8 - repo: http://github.com/PyCQA/flake8
rev: 6.0.0 rev: 6.1.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:

View File

@@ -2,44 +2,44 @@
© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| Name | Version | License | Description | URL | | Name | Supported Version | Built Version | License | Description | URL |
+===================+=========+====================================+=======================================================================================================+====================================================================+ +===================+=====================+===============+======================================+========================================================================================================+=====================================================================+
| gymnasium | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org | | gymnasium | 0.28.1 | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| ipywidgets | 8.1.5 | BSD License | Jupyter interactive widgets | http://jupyter.org | | ipywidgets | ~=8.0 | 8.1.5 | BSD License | Jupyter interactive widgets | http://jupyter.org |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| jupyterlab | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org | | jupyterlab | 3.6.1 | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| kaleido | 0.2.1 | MIT | Static image export for web-based visualization libraries with zero dependencies | https://github.com/plotly/Kaleido | | kaleido | ==0.2.1 | 0.2.1 | MIT | Static image export for web-based visualization libraries with zero dependencies | https://github.com/plotly/Kaleido |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| matplotlib | 3.7.1 | Python Software Foundation License | Python plotting package | https://matplotlib.org | | matplotlib | >=3.7.1 | 3.7.1 | Python Software Foundation License | Python plotting package | https://matplotlib.org |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| networkx | 3.1 | BSD License | Python package for creating and manipulating graphs and networks | https://networkx.org/ | | networkx | 3.1 | 3.1 | BSD License | Python package for creating and manipulating graphs and networks | https://networkx.org/ |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| numpy | 1.23.5 | BSD License | NumPy is the fundamental package for array computing with Python. | https://www.numpy.org | | numpy | ~1.23 | 1.23.5 | BSD License | NumPy is the fundamental package for array computing with Python. | https://www.numpy.org |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| platformdirs | 3.5.1 | MIT License | A small Python package for determining appropriate platform-specific dirs, e.g. a "user data dir". | https://github.com/platformdirs/platformdirs | | platformdirs | 3.5.1 | 3.5.1 | MIT License | A small Python package for determining appropriate platform-specific dirs, e.g. a "user data dir". | https://github.com/platformdirs/platformdirs |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| plotly | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ | | plotly | 5.15 | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| polars | 0.20.30 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | | polars | 0.20.30 | 0.20.30 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| prettytable | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable | | prettytable | 3.8.0 | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| pydantic | 2.7.0 | MIT License | Data validation using Python type hints | https://github.com/pydantic/pydantic | | pydantic | 2.7.0 | 2.7.0 | MIT License | Data validation using Python type hints | https://github.com/pydantic/pydantic |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| PyYAML | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ | | PyYAML | >=6.0 | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| ray | 2.32.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | | ray | >=2.20, <2.33 | 2.32.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| stable-baselines3 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 | | stable-baselines3 | 2.1.0 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| tensorflow | 2.12.0 | Apache Software License | TensorFlow is an open source machine learning framework for everyone. | https://www.tensorflow.org/ | | tensorflow | ~=2.12 | 2.12.0 | Apache Software License | TensorFlow is an open source machine learning framework for everyone. | https://www.tensorflow.org/ |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| typer | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer | | typer | >=0.9 | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| Deepdiff | 8.0.1 | MIT License | Deep difference of dictionaries, iterables, strings, and any other object objects. | https://github.com/seperman/deepdiff | | Deepdiff | 8.0.1 | 8.0.1 | MIT License | Deep difference of dictionaries, iterables, strings, and any other object objects. | https://github.com/seperman/deepdiff |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+
| sb3_contrib | 2.1.0 | MIT License | Contrib package for Stable-Baselines3 - Experimental reinforcement learning (RL) code (Action Masking)| https://github.com/Stable-Baselines-Team/stable-baselines3-contrib | | sb3_contrib | 2.1.0 | 2.1.0 | MIT License | Contrib package for Stable-Baselines3 - Experimental reinforcement learning (RL) code (Action Masking) | https://github.com/Stable-Baselines-Team/stable-baselines3-contrib |
+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ +-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+

View File

@@ -7,7 +7,7 @@ name = "primaite"
description = "PrimAITE (Primary-level AI Training Environment) is a simulation environment for training AI under the ARCD programme." description = "PrimAITE (Primary-level AI Training Environment) is a simulation environment for training AI under the ARCD programme."
authors = [{name="Defence Science and Technology Laboratory UK", email="oss@dstl.gov.uk"}] authors = [{name="Defence Science and Technology Laboratory UK", email="oss@dstl.gov.uk"}]
license = {file = "LICENSE"} license = {file = "LICENSE"}
requires-python = ">=3.9, <3.12" requires-python = ">=3.9, <3.13"
dynamic = ["version", "readme"] dynamic = ["version", "readme"]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
@@ -26,15 +26,15 @@ dependencies = [
"gymnasium==0.28.1", "gymnasium==0.28.1",
"jupyterlab==3.6.1", "jupyterlab==3.6.1",
"kaleido==0.2.1", "kaleido==0.2.1",
"matplotlib==3.7.1", "matplotlib>=3.7.1",
"networkx==3.1", "networkx==3.1",
"numpy==1.23.5", "numpy~=1.23",
"platformdirs==3.5.1", "platformdirs==3.5.1",
"plotly==5.15.0", "plotly==5.15.0",
"polars==0.20.30", "polars==0.20.30",
"prettytable==3.8.0", "prettytable==3.8.0",
"PyYAML==6.0", "PyYAML>=6.0",
"typer[all]==0.9.0", "typer[all]>=0.9",
"pydantic==2.7.0", "pydantic==2.7.0",
"ipywidgets", "ipywidgets",
"deepdiff" "deepdiff"
@@ -53,8 +53,8 @@ license-files = ["LICENSE"]
[project.optional-dependencies] [project.optional-dependencies]
rl = [ rl = [
"ray[rllib] >= 2.20.0, <2.33", "ray[rllib] >= 2.20.0, <2.33",
"tensorflow==2.12.0", "tensorflow~=2.12",
"stable-baselines3[extra]==2.1.0", "stable-baselines3==2.1.0",
"sb3-contrib==2.1.0", "sb3-contrib==2.1.0",
] ]
dev = [ dev = [
@@ -69,7 +69,7 @@ dev = [
"pytest-xdist==3.3.1", "pytest-xdist==3.3.1",
"pytest-cov==4.0.0", "pytest-cov==4.0.0",
"pytest-flake8==1.1.1", "pytest-flake8==1.1.1",
"setuptools==66", "setuptools==75.6.0",
"Sphinx==7.1.2", "Sphinx==7.1.2",
"sphinx-copybutton==0.5.2", "sphinx-copybutton==0.5.2",
"wheel==0.38.4", "wheel==0.38.4",

View File

@@ -161,8 +161,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -153,8 +153,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP
@@ -668,8 +668,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -93,7 +93,7 @@ class AgentLog:
def _write_to_terminal(self, msg: str, level: str, to_terminal: bool = False): def _write_to_terminal(self, msg: str, level: str, to_terminal: bool = False):
if to_terminal or SIM_OUTPUT.write_agent_log_to_terminal: if to_terminal or SIM_OUTPUT.write_agent_log_to_terminal:
print(f"{self.agent_name}: ({ self.timestep}) ({level}) {msg}") print(f"{self.agent_name}: ({self.timestep}) ({level}) {msg}")
def debug(self, msg: str, to_terminal: bool = False): def debug(self, msg: str, to_terminal: bool = False):
""" """

View File

@@ -24,8 +24,8 @@ class ACLObservation(AbstractObservation, identifier="ACL"):
"""List of IP addresses.""" """List of IP addresses."""
wildcard_list: Optional[List[str]] = None wildcard_list: Optional[List[str]] = None
"""List of wildcard strings.""" """List of wildcard strings."""
port_list: Optional[List[int]] = None port_list: Optional[List[str]] = None
"""List of port numbers.""" """List of port names."""
protocol_list: Optional[List[str]] = None protocol_list: Optional[List[str]] = None
"""List of protocol names.""" """List of protocol names."""
num_rules: Optional[int] = None num_rules: Optional[int] = None
@@ -37,7 +37,7 @@ class ACLObservation(AbstractObservation, identifier="ACL"):
num_rules: int, num_rules: int,
ip_list: List[IPv4Address], ip_list: List[IPv4Address],
wildcard_list: List[str], wildcard_list: List[str],
port_list: List[int], port_list: List[str],
protocol_list: List[str], protocol_list: List[str],
) -> None: ) -> None:
""" """
@@ -51,8 +51,8 @@ class ACLObservation(AbstractObservation, identifier="ACL"):
:type ip_list: List[IPv4Address] :type ip_list: List[IPv4Address]
:param wildcard_list: List of wildcard strings. :param wildcard_list: List of wildcard strings.
:type wildcard_list: List[str] :type wildcard_list: List[str]
:param port_list: List of port numbers. :param port_list: List of port names.
:type port_list: List[int] :type port_list: List[str]
:param protocol_list: List of protocol names. :param protocol_list: List of protocol names.
:type protocol_list: List[str] :type protocol_list: List[str]
""" """
@@ -60,7 +60,7 @@ class ACLObservation(AbstractObservation, identifier="ACL"):
self.num_rules: int = num_rules self.num_rules: int = num_rules
self.ip_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(ip_list)} self.ip_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(ip_list)}
self.wildcard_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(wildcard_list)} self.wildcard_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(wildcard_list)}
self.port_to_id: Dict[int, int] = {p: i + 2 for i, p in enumerate(port_list)} self.port_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(port_list)}
self.protocol_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(protocol_list)} self.protocol_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(protocol_list)}
self.default_observation: Dict = { self.default_observation: Dict = {
i i

View File

@@ -190,6 +190,8 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"):
if self.files: if self.files:
self.default_observation["FILES"] = {i + 1: f.default_observation for i, f in enumerate(self.files)} self.default_observation["FILES"] = {i + 1: f.default_observation for i, f in enumerate(self.files)}
self.cached_obs: Optional[ObsType] = self.default_observation
def observe(self, state: Dict) -> ObsType: def observe(self, state: Dict) -> ObsType:
""" """
Generate observation based on the current state of the simulation. Generate observation based on the current state of the simulation.
@@ -204,7 +206,10 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"):
return self.default_observation return self.default_observation
if self.file_system_requires_scan: if self.file_system_requires_scan:
health_status = folder_state["visible_status"] if not folder_state["scanned_this_step"]:
health_status = self.cached_obs["health_status"]
else:
health_status = folder_state["visible_status"]
else: else:
health_status = folder_state["health_status"] health_status = folder_state["health_status"]

View File

@@ -27,13 +27,13 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"):
"""List of IP addresses for encoding ACLs.""" """List of IP addresses for encoding ACLs."""
wildcard_list: Optional[List[str]] = None wildcard_list: Optional[List[str]] = None
"""List of IP wildcards for encoding ACLs.""" """List of IP wildcards for encoding ACLs."""
port_list: Optional[List[int]] = None port_list: Optional[List[str]] = None
"""List of ports for encoding ACLs.""" """List of ports for encoding ACLs."""
protocol_list: Optional[List[str]] = None protocol_list: Optional[List[str]] = None
"""List of protocols for encoding ACLs.""" """List of protocols for encoding ACLs."""
num_rules: Optional[int] = None num_rules: Optional[int] = None
"""Number of rules ACL rules to show.""" """Number of rules ACL rules to show."""
include_users: Optional[bool] = True include_users: Optional[bool] = None
"""If True, report user session information.""" """If True, report user session information."""
def __init__( def __init__(
@@ -41,7 +41,7 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"):
where: WhereType, where: WhereType,
ip_list: List[str], ip_list: List[str],
wildcard_list: List[str], wildcard_list: List[str],
port_list: List[int], port_list: List[str],
protocol_list: List[str], protocol_list: List[str],
num_rules: int, num_rules: int,
include_users: bool, include_users: bool,
@@ -56,8 +56,8 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"):
:type ip_list: List[str] :type ip_list: List[str]
:param wildcard_list: List of wildcard rules. :param wildcard_list: List of wildcard rules.
:type wildcard_list: List[str] :type wildcard_list: List[str]
:param port_list: List of port numbers. :param port_list: List of port names.
:type port_list: List[int] :type port_list: List[str]
:param protocol_list: List of protocol types. :param protocol_list: List of protocol types.
:type protocol_list: List[str] :type protocol_list: List[str]
:param num_rules: Number of rules configured in the firewall. :param num_rules: Number of rules configured in the firewall.
@@ -140,6 +140,8 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"):
}, },
}, },
} }
if self.include_users:
self.default_observation["users"] = {"local_login": 0, "remote_sessions": 0}
def observe(self, state: Dict) -> ObsType: def observe(self, state: Dict) -> ObsType:
""" """
@@ -153,29 +155,35 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"):
firewall_state = access_from_nested_dict(state, self.where) firewall_state = access_from_nested_dict(state, self.where)
if firewall_state is NOT_PRESENT_IN_STATE: if firewall_state is NOT_PRESENT_IN_STATE:
return self.default_observation return self.default_observation
obs = {
"PORTS": {i + 1: p.observe(state) for i, p in enumerate(self.ports)}, is_on = firewall_state["operating_state"] == 1
"ACL": { if not is_on:
"INTERNAL": { obs = {**self.default_observation}
"INBOUND": self.internal_inbound_acl.observe(state),
"OUTBOUND": self.internal_outbound_acl.observe(state), else:
obs = {
"PORTS": {i + 1: p.observe(state) for i, p in enumerate(self.ports)},
"ACL": {
"INTERNAL": {
"INBOUND": self.internal_inbound_acl.observe(state),
"OUTBOUND": self.internal_outbound_acl.observe(state),
},
"DMZ": {
"INBOUND": self.dmz_inbound_acl.observe(state),
"OUTBOUND": self.dmz_outbound_acl.observe(state),
},
"EXTERNAL": {
"INBOUND": self.external_inbound_acl.observe(state),
"OUTBOUND": self.external_outbound_acl.observe(state),
},
}, },
"DMZ": {
"INBOUND": self.dmz_inbound_acl.observe(state),
"OUTBOUND": self.dmz_outbound_acl.observe(state),
},
"EXTERNAL": {
"INBOUND": self.external_inbound_acl.observe(state),
"OUTBOUND": self.external_outbound_acl.observe(state),
},
},
}
if self.include_users:
sess = firewall_state["services"]["UserSessionManager"]
obs["users"] = {
"local_login": 1 if sess["current_local_user"] else 0,
"remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])),
} }
if self.include_users:
sess = firewall_state["services"]["UserSessionManager"]
obs["users"] = {
"local_login": 1 if sess["current_local_user"] else 0,
"remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])),
}
return obs return obs
@property @property
@@ -186,34 +194,36 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"):
:return: Gymnasium space representing the observation space for firewall status. :return: Gymnasium space representing the observation space for firewall status.
:rtype: spaces.Space :rtype: spaces.Space
""" """
space = spaces.Dict( shape = {
{ "PORTS": spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)}),
"PORTS": spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)}), "ACL": spaces.Dict(
"ACL": spaces.Dict( {
{ "INTERNAL": spaces.Dict(
"INTERNAL": spaces.Dict( {
{ "INBOUND": self.internal_inbound_acl.space,
"INBOUND": self.internal_inbound_acl.space, "OUTBOUND": self.internal_outbound_acl.space,
"OUTBOUND": self.internal_outbound_acl.space, }
} ),
), "DMZ": spaces.Dict(
"DMZ": spaces.Dict( {
{ "INBOUND": self.dmz_inbound_acl.space,
"INBOUND": self.dmz_inbound_acl.space, "OUTBOUND": self.dmz_outbound_acl.space,
"OUTBOUND": self.dmz_outbound_acl.space, }
} ),
), "EXTERNAL": spaces.Dict(
"EXTERNAL": spaces.Dict( {
{ "INBOUND": self.external_inbound_acl.space,
"INBOUND": self.external_inbound_acl.space, "OUTBOUND": self.external_outbound_acl.space,
"OUTBOUND": self.external_outbound_acl.space, }
} ),
), }
} ),
), }
} if self.include_users:
) shape["users"] = spaces.Dict(
return space {"local_login": spaces.Discrete(2), "remote_sessions": spaces.Discrete(self.max_users + 1)}
)
return spaces.Dict(shape)
@classmethod @classmethod
def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FirewallObservation: def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FirewallObservation:

View File

@@ -54,7 +54,7 @@ class HostObservation(AbstractObservation, identifier="HOST"):
""" """
If True, files and folders must be scanned to update the health state. If False, true state is always shown. If True, files and folders must be scanned to update the health state. If False, true state is always shown.
""" """
include_users: Optional[bool] = True include_users: Optional[bool] = None
"""If True, report user session information.""" """If True, report user session information."""
def __init__( def __init__(
@@ -191,25 +191,31 @@ class HostObservation(AbstractObservation, identifier="HOST"):
if node_state is NOT_PRESENT_IN_STATE: if node_state is NOT_PRESENT_IN_STATE:
return self.default_observation return self.default_observation
obs = {} is_on = node_state["operating_state"] == 1
if not is_on:
obs = {**self.default_observation}
else:
obs = {}
if self.services:
obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)}
if self.applications:
obs["APPLICATIONS"] = {i + 1: app.observe(state) for i, app in enumerate(self.applications)}
if self.folders:
obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)}
if self.nics:
obs["NICS"] = {i + 1: nic.observe(state) for i, nic in enumerate(self.nics)}
if self.include_num_access:
obs["num_file_creations"] = node_state["file_system"]["num_file_creations"]
obs["num_file_deletions"] = node_state["file_system"]["num_file_deletions"]
if self.include_users:
sess = node_state["services"]["UserSessionManager"]
obs["users"] = {
"local_login": 1 if sess["current_local_user"] else 0,
"remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])),
}
obs["operating_status"] = node_state["operating_state"] obs["operating_status"] = node_state["operating_state"]
if self.services:
obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)}
if self.applications:
obs["APPLICATIONS"] = {i + 1: app.observe(state) for i, app in enumerate(self.applications)}
if self.folders:
obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)}
if self.nics:
obs["NICS"] = {i + 1: nic.observe(state) for i, nic in enumerate(self.nics)}
if self.include_num_access:
obs["num_file_creations"] = node_state["file_system"]["num_file_creations"]
obs["num_file_deletions"] = node_state["file_system"]["num_file_deletions"]
if self.include_users:
sess = node_state["services"]["UserSessionManager"]
obs["users"] = {
"local_login": 1 if sess["current_local_user"] else 0,
"remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])),
}
return obs return obs
@property @property

View File

@@ -56,7 +56,7 @@ class NodesObservation(AbstractObservation, identifier="NODES"):
"""List of IP addresses for encoding ACLs.""" """List of IP addresses for encoding ACLs."""
wildcard_list: Optional[List[str]] = None wildcard_list: Optional[List[str]] = None
"""List of IP wildcards for encoding ACLs.""" """List of IP wildcards for encoding ACLs."""
port_list: Optional[List[int]] = None port_list: Optional[List[str]] = None
"""List of ports for encoding ACLs.""" """List of ports for encoding ACLs."""
protocol_list: Optional[List[str]] = None protocol_list: Optional[List[str]] = None
"""List of protocols for encoding ACLs.""" """List of protocols for encoding ACLs."""

View File

@@ -33,13 +33,13 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"):
"""List of IP addresses for encoding ACLs.""" """List of IP addresses for encoding ACLs."""
wildcard_list: Optional[List[str]] = None wildcard_list: Optional[List[str]] = None
"""List of IP wildcards for encoding ACLs.""" """List of IP wildcards for encoding ACLs."""
port_list: Optional[List[int]] = None port_list: Optional[List[str]] = None
"""List of ports for encoding ACLs.""" """List of ports for encoding ACLs."""
protocol_list: Optional[List[str]] = None protocol_list: Optional[List[str]] = None
"""List of protocols for encoding ACLs.""" """List of protocols for encoding ACLs."""
num_rules: Optional[int] = None num_rules: Optional[int] = None
"""Number of rules ACL rules to show.""" """Number of rules ACL rules to show."""
include_users: Optional[bool] = True include_users: Optional[bool] = None
"""If True, report user session information.""" """If True, report user session information."""
def __init__( def __init__(
@@ -84,6 +84,8 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"):
} }
if self.ports: if self.ports:
self.default_observation["PORTS"] = {i + 1: p.default_observation for i, p in enumerate(self.ports)} self.default_observation["PORTS"] = {i + 1: p.default_observation for i, p in enumerate(self.ports)}
if self.include_users:
self.default_observation["users"] = {"local_login": 0, "remote_sessions": 0}
def observe(self, state: Dict) -> ObsType: def observe(self, state: Dict) -> ObsType:
""" """
@@ -98,16 +100,21 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"):
if router_state is NOT_PRESENT_IN_STATE: if router_state is NOT_PRESENT_IN_STATE:
return self.default_observation return self.default_observation
obs = {} is_on = router_state["operating_state"] == 1
obs["ACL"] = self.acl.observe(state) if not is_on:
if self.ports: obs = {**self.default_observation}
obs["PORTS"] = {i + 1: p.observe(state) for i, p in enumerate(self.ports)}
if self.include_users: else:
sess = router_state["services"]["UserSessionManager"] obs = {}
obs["users"] = { obs["ACL"] = self.acl.observe(state)
"local_login": 1 if sess["current_local_user"] else 0, if self.ports:
"remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), obs["PORTS"] = {i + 1: p.observe(state) for i, p in enumerate(self.ports)}
} if self.include_users:
sess = router_state["services"]["UserSessionManager"]
obs["users"] = {
"local_login": 1 if sess["current_local_user"] else 0,
"remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])),
}
return obs return obs
@property @property
@@ -121,6 +128,10 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"):
shape = {"ACL": self.acl.space} shape = {"ACL": self.acl.space}
if self.ports: if self.ports:
shape["PORTS"] = spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)}) shape["PORTS"] = spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)})
if self.include_users:
shape["users"] = spaces.Dict(
{"local_login": spaces.Discrete(2), "remote_sessions": spaces.Discrete(self.max_users + 1)}
)
return spaces.Dict(shape) return spaces.Dict(shape)
@classmethod @classmethod

View File

@@ -258,6 +258,7 @@ class PrimaiteGame:
net = sim.network net = sim.network
simulation_config = cfg.get("simulation", {}) simulation_config = cfg.get("simulation", {})
defaults_config = cfg.get("defaults", {})
network_config = simulation_config.get("network", {}) network_config = simulation_config.get("network", {})
airspace_cfg = network_config.get("airspace", {}) airspace_cfg = network_config.get("airspace", {})
frequency_max_capacity_mbps_cfg = airspace_cfg.get("frequency_max_capacity_mbps", {}) frequency_max_capacity_mbps_cfg = airspace_cfg.get("frequency_max_capacity_mbps", {})
@@ -338,6 +339,18 @@ class PrimaiteGame:
_LOGGER.error(msg) _LOGGER.error(msg)
raise ValueError(msg) raise ValueError(msg)
# TODO: handle simulation defaults more cleanly
if "node_start_up_duration" in defaults_config:
new_node.start_up_duration = defaults_config["node_startup_duration"]
if "node_shut_down_duration" in defaults_config:
new_node.shut_down_duration = defaults_config["node_shut_down_duration"]
if "node_scan_duration" in defaults_config:
new_node.node_scan_duration = defaults_config["node_scan_duration"]
if "folder_scan_duration" in defaults_config:
new_node.file_system._default_folder_scan_duration = defaults_config["folder_scan_duration"]
if "folder_restore_duration" in defaults_config:
new_node.file_system._default_folder_restore_duration = defaults_config["folder_restore_duration"]
if "users" in node_cfg and new_node.software_manager.software.get("UserManager"): if "users" in node_cfg and new_node.software_manager.software.get("UserManager"):
user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa
for user_cfg in node_cfg["users"]: for user_cfg in node_cfg["users"]:
@@ -384,6 +397,15 @@ class PrimaiteGame:
msg = f"Configuration contains an invalid service type: {service_type}" msg = f"Configuration contains an invalid service type: {service_type}"
_LOGGER.error(msg) _LOGGER.error(msg)
raise ValueError(msg) raise ValueError(msg)
# TODO: handle simulation defaults more cleanly
if "service_fix_duration" in defaults_config:
new_service.fixing_duration = defaults_config["service_fix_duration"]
if "service_restart_duration" in defaults_config:
new_service.restart_duration = defaults_config["service_restart_duration"]
if "service_install_duration" in defaults_config:
new_service.install_duration = defaults_config["service_install_duration"]
# service-dependent options # service-dependent options
if service_type == "DNSClient": if service_type == "DNSClient":
if "options" in service_cfg: if "options" in service_cfg:

View File

@@ -11,6 +11,15 @@
"PrimAITE environments support action masking. The action mask shows which of the agent's actions are applicable with the current environment state. For example, a node can only be turned on if it is currently turned off." "PrimAITE environments support action masking. The action mask shows which of the agent's actions are applicable with the current environment state. For example, a node can only be turned on if it is currently turned off."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -15,6 +15,15 @@
"*(For a full explanation of the Data Manipulation scenario, check out the data manipulation scenario notebook)*" "*(For a full explanation of the Data Manipulation scenario, check out the data manipulation scenario notebook)*"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -371,6 +371,15 @@
"First, load the required modules" "First, load the required modules"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -9,6 +9,15 @@
"© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n" "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -25,6 +25,15 @@
"Let's set up a minimal network simulation and send some requests to see how it works." "Let's set up a minimal network simulation and send some requests to see how it works."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -18,6 +18,15 @@
"The Terminal service comes pre-installed on most Nodes (The exception being Switches, as these are currently dumb). " "The Terminal service comes pre-installed on most Nodes (The exception being Switches, as these are currently dumb). "
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -18,6 +18,15 @@
"#### First, Import packages and read our config file." "#### First, Import packages and read our config file."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -32,8 +41,6 @@
"from ray.rllib.algorithms.ppo import PPOConfig\n", "from ray.rllib.algorithms.ppo import PPOConfig\n",
"from primaite.session.ray_envs import PrimaiteRayMARLEnv\n", "from primaite.session.ray_envs import PrimaiteRayMARLEnv\n",
"\n", "\n",
"# If you get an error saying this config file doesn't exist, you may need to run `primaite setup` in your command line\n",
"# to copy the files to your user data path.\n",
"with open(PRIMAITE_PATHS.user_config_path / 'example_config/data_manipulation_marl.yaml', 'r') as f:\n", "with open(PRIMAITE_PATHS.user_config_path / 'example_config/data_manipulation_marl.yaml', 'r') as f:\n",
" cfg = yaml.safe_load(f)\n", " cfg = yaml.safe_load(f)\n",
"\n", "\n",

View File

@@ -11,6 +11,15 @@
"This notebook will demonstrate how to use PrimaiteRayEnv to train a basic PPO agent." "This notebook will demonstrate how to use PrimaiteRayEnv to train a basic PPO agent."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -18,6 +18,15 @@
"#### First, we import the inital packages and read in our configuration file." "#### First, we import the inital packages and read in our configuration file."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -18,6 +18,15 @@
"Import packages and read config file." "Import packages and read config file."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!primaite setup"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

@@ -30,6 +30,11 @@ class FileSystem(SimComponent):
num_file_deletions: int = 0 num_file_deletions: int = 0
"Number of file deletions in the current step." "Number of file deletions in the current step."
_default_folder_scan_duration: Optional[int] = None
"Override default scan duration for folders"
_default_folder_restore_duration: Optional[int] = None
"Override default restore duration for folders"
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Ensure a default root folder # Ensure a default root folder
@@ -258,6 +263,11 @@ class FileSystem(SimComponent):
name=folder.name, request_type=RequestType(func=folder._request_manager) name=folder.name, request_type=RequestType(func=folder._request_manager)
) )
self.folders[folder.uuid] = folder self.folders[folder.uuid] = folder
# set the folder scan and restore durations.
if self._default_folder_scan_duration is not None:
folder.scan_duration = self._default_folder_scan_duration
if self._default_folder_restore_duration is not None:
folder.restore_duration = self._default_folder_restore_duration
return folder return folder
def delete_folder(self, folder_name: str) -> bool: def delete_folder(self, folder_name: str) -> bool:

View File

@@ -43,6 +43,9 @@ def convert_size(size_bytes: int) -> str:
class FileSystemItemHealthStatus(Enum): class FileSystemItemHealthStatus(Enum):
"""Status of the FileSystemItem.""" """Status of the FileSystemItem."""
NONE = 0
"""File system item health status is not known."""
GOOD = 1 GOOD = 1
"""File/Folder is OK.""" """File/Folder is OK."""
@@ -72,7 +75,7 @@ class FileSystemItemABC(SimComponent):
health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.GOOD health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.GOOD
"Actual status of the current FileSystemItem" "Actual status of the current FileSystemItem"
visible_health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.GOOD visible_health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.NONE
"Visible status of the current FileSystemItem" "Visible status of the current FileSystemItem"
previous_hash: Optional[str] = None previous_hash: Optional[str] = None

View File

@@ -46,7 +46,7 @@ class Folder(FileSystemItemABC):
:param sys_log: The SysLog instance to us to create system logs. :param sys_log: The SysLog instance to us to create system logs.
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
self._scanned_this_step: bool = False
self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})") self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})")
def _init_request_manager(self) -> RequestManager: def _init_request_manager(self) -> RequestManager:
@@ -83,6 +83,7 @@ class Folder(FileSystemItemABC):
state = super().describe_state() state = super().describe_state()
state["files"] = {file.name: file.describe_state() for uuid, file in self.files.items()} state["files"] = {file.name: file.describe_state() for uuid, file in self.files.items()}
state["deleted_files"] = {file.name: file.describe_state() for uuid, file in self.deleted_files.items()} state["deleted_files"] = {file.name: file.describe_state() for uuid, file in self.deleted_files.items()}
state["scanned_this_step"] = self._scanned_this_step
return state return state
def show(self, markdown: bool = False): def show(self, markdown: bool = False):
@@ -135,7 +136,7 @@ class Folder(FileSystemItemABC):
def pre_timestep(self, timestep: int) -> None: def pre_timestep(self, timestep: int) -> None:
"""Apply pre-timestep logic.""" """Apply pre-timestep logic."""
super().pre_timestep(timestep) super().pre_timestep(timestep)
self._scanned_this_step = False
for file in self.files.values(): for file in self.files.values():
file.pre_timestep(timestep) file.pre_timestep(timestep)
@@ -148,9 +149,17 @@ class Folder(FileSystemItemABC):
for file_id in self.files: for file_id in self.files:
file = self.get_file_by_id(file_uuid=file_id) file = self.get_file_by_id(file_uuid=file_id)
file.scan() file.scan()
if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: # set folder health to worst file's health by generating a list of file healths. If no files, use 0
self.health_status = FileSystemItemHealthStatus.CORRUPT self.health_status = FileSystemItemHealthStatus(
max(
[f.health_status.value for f in self.files.values()]
or [
0,
]
)
)
self.visible_health_status = self.health_status self.visible_health_status = self.health_status
self._scanned_this_step = True
def _reveal_to_red_timestep(self) -> None: def _reveal_to_red_timestep(self) -> None:
"""Apply reveal to red timestep.""" """Apply reveal to red timestep."""

View File

@@ -118,6 +118,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"):
session_id: Optional[str] = None, session_id: Optional[str] = None,
is_reattempt: Optional[bool] = False, is_reattempt: Optional[bool] = False,
) -> bool: ) -> bool:
self._active = True
""" """
Connects the client to a given FTP server. Connects the client to a given FTP server.
@@ -174,6 +175,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"):
:param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False.
:type: is_reattempt: Optional[bool] :type: is_reattempt: Optional[bool]
""" """
self._active = True
# send a disconnect request payload to FTP server # send a disconnect request payload to FTP server
payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT)
software_manager: SoftwareManager = self.software_manager software_manager: SoftwareManager = self.software_manager
@@ -219,6 +221,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"):
:param: session_id: The id of the session :param: session_id: The id of the session
:type: session_id: Optional[str] :type: session_id: Optional[str]
""" """
self._active = True
# check if the file to transfer exists on the client # check if the file to transfer exists on the client
file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name)
if not file_to_transfer: if not file_to_transfer:
@@ -276,6 +279,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"):
:param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"].
:type: dest_port: Optional[int] :type: dest_port: Optional[int]
""" """
self._active = True
# check if FTP is currently connected to IP # check if FTP is currently connected to IP
self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port)
@@ -327,6 +331,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"):
This helps prevent an FTP request loop - FTP client and servers can exist on This helps prevent an FTP request loop - FTP client and servers can exist on
the same node. the same node.
""" """
self._active = True
if not self._can_perform_action(): if not self._can_perform_action():
return False return False

View File

@@ -3,9 +3,11 @@ from abc import ABC
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Dict, Optional from typing import Dict, Optional
from pydantic import StrictBool
from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system import File
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.service import Service, ServiceOperatingState
from primaite.utils.validation.port import Port from primaite.utils.validation.port import Port
@@ -16,9 +18,22 @@ class FTPServiceABC(Service, ABC):
Contains shared methods between both classes. Contains shared methods between both classes.
""" """
_active: StrictBool = False
"""Flag that is True on timesteps where service transmits data and False when idle. Used for describe_state."""
def pre_timestep(self, timestep: int) -> None:
"""When a new timestep begins, clear the _active attribute."""
self._active = False
return super().pre_timestep(timestep)
def describe_state(self) -> Dict: def describe_state(self) -> Dict:
"""Returns a Dict of the FTPService state.""" """Returns a Dict of the FTPService state."""
return super().describe_state() state = super().describe_state()
# override so that the service is shows as running only if actively transmitting data this timestep
if self.operating_state == ServiceOperatingState.RUNNING and not self._active:
state["operating_state"] = ServiceOperatingState.STOPPED.value
return state
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket:
""" """
@@ -29,6 +44,7 @@ class FTPServiceABC(Service, ABC):
:param: session_id: session ID linked to the FTP Packet. Optional. :param: session_id: session ID linked to the FTP Packet. Optional.
:type: session_id: Optional[str] :type: session_id: Optional[str]
""" """
self._active = True
if payload.ftp_command is not None: if payload.ftp_command is not None:
self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.") self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.")
@@ -51,6 +67,7 @@ class FTPServiceABC(Service, ABC):
:param: payload: The FTP Packet that contains the file data :param: payload: The FTP Packet that contains the file data
:type: FTPPacket :type: FTPPacket
""" """
self._active = True
try: try:
file_name = payload.ftp_command_args["dest_file_name"] file_name = payload.ftp_command_args["dest_file_name"]
folder_name = payload.ftp_command_args["dest_folder_name"] folder_name = payload.ftp_command_args["dest_folder_name"]
@@ -106,6 +123,7 @@ class FTPServiceABC(Service, ABC):
:param: is_response: is true if the data being sent is in response to a request. Default False. :param: is_response: is true if the data being sent is in response to a request. Default False.
:type: is_response: bool :type: is_response: bool
""" """
self._active = True
# send STOR request # send STOR request
payload: FTPPacket = FTPPacket( payload: FTPPacket = FTPPacket(
ftp_command=FTPCommand.STOR, ftp_command=FTPCommand.STOR,
@@ -135,6 +153,7 @@ class FTPServiceABC(Service, ABC):
:param: payload: The FTP Packet that contains the file data :param: payload: The FTP Packet that contains the file data
:type: FTPPacket :type: FTPPacket
""" """
self._active = True
try: try:
# find the file # find the file
file_name = payload.ftp_command_args["src_file_name"] file_name = payload.ftp_command_args["src_file_name"]
@@ -181,6 +200,7 @@ class FTPServiceABC(Service, ABC):
:return: True if successful, False otherwise. :return: True if successful, False otherwise.
""" """
self._active = True
self.sys_log.info(f"{self.name}: Sending FTP {payload.ftp_command.name} {payload.ftp_command_args}") self.sys_log.info(f"{self.name}: Sending FTP {payload.ftp_command.name} {payload.ftp_command_args}")
return super().send( return super().send(

View File

@@ -69,8 +69,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -74,8 +74,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -88,8 +88,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -160,8 +160,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -102,8 +102,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -161,8 +161,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -77,8 +77,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -81,8 +81,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -152,8 +152,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP
@@ -666,8 +666,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -151,8 +151,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -81,8 +81,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -155,8 +155,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -103,8 +103,8 @@ agents:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP

View File

@@ -69,7 +69,7 @@ def test_file_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent
file.corrupt() file.corrupt()
assert file.health_status == FileSystemItemHealthStatus.CORRUPT assert file.health_status == FileSystemItemHealthStatus.CORRUPT
assert file.visible_health_status == FileSystemItemHealthStatus.GOOD assert file.visible_health_status == FileSystemItemHealthStatus.NONE
action = ( action = (
"node_file_scan", "node_file_scan",

View File

@@ -52,12 +52,12 @@ def test_folder_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge
folder = client_1.file_system.get_folder(folder_name="downloads") folder = client_1.file_system.get_folder(folder_name="downloads")
assert folder.health_status == FileSystemItemHealthStatus.GOOD assert folder.health_status == FileSystemItemHealthStatus.GOOD
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
folder.corrupt() folder.corrupt()
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
action = ( action = (
"node_folder_scan", "node_folder_scan",

View File

@@ -32,11 +32,11 @@ def test_file_observation(simulation):
assert dog_file_obs.space["health_status"] == spaces.Discrete(6) assert dog_file_obs.space["health_status"] == spaces.Discrete(6)
observation_state = dog_file_obs.observe(simulation.describe_state()) observation_state = dog_file_obs.observe(simulation.describe_state())
assert observation_state.get("health_status") == 1 # good initial assert observation_state.get("health_status") == 0 # initially unset
file.corrupt() file.corrupt()
observation_state = dog_file_obs.observe(simulation.describe_state()) observation_state = dog_file_obs.observe(simulation.describe_state())
assert observation_state.get("health_status") == 1 # scan file so this changes assert observation_state.get("health_status") == 0 # still default unset value because no scan happened
file.scan() file.scan()
file.apply_timestep(0) # apply time step file.apply_timestep(0) # apply time step
@@ -63,11 +63,11 @@ def test_folder_observation(simulation):
observation_state = root_folder_obs.observe(simulation.describe_state()) observation_state = root_folder_obs.observe(simulation.describe_state())
assert observation_state.get("FILES") is not None assert observation_state.get("FILES") is not None
assert observation_state.get("health_status") == 1 assert observation_state.get("health_status") == 0 # initially unset
file.corrupt() # corrupt just the file file.corrupt() # corrupt just the file
observation_state = root_folder_obs.observe(simulation.describe_state()) observation_state = root_folder_obs.observe(simulation.describe_state())
assert observation_state.get("health_status") == 1 # scan folder to change this assert observation_state.get("health_status") == 0 # still unset as no scan occurred yet
folder.scan() folder.scan()
for i in range(folder.scan_duration + 1): for i in range(folder.scan_duration + 1):

View File

@@ -275,7 +275,7 @@ def test_node_file_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAge
client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1 = game.simulation.network.get_node_by_hostname("client_1")
file = client_1.file_system.get_file("downloads", "cat.png") file = client_1.file_system.get_file("downloads", "cat.png")
assert file.health_status == FileSystemItemHealthStatus.GOOD assert file.health_status == FileSystemItemHealthStatus.GOOD
assert file.visible_health_status == FileSystemItemHealthStatus.GOOD assert file.visible_health_status == FileSystemItemHealthStatus.NONE
# 2: perform a scan and make sure nothing has changed # 2: perform a scan and make sure nothing has changed
action = ( action = (

View File

@@ -17,12 +17,7 @@ def test_file_observation():
dog_file_obs = FileObservation( dog_file_obs = FileObservation(
where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"], where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"],
include_num_access=False, include_num_access=False,
file_system_requires_scan=True, file_system_requires_scan=False,
) )
assert dog_file_obs.observe(state) == {"health_status": 1} assert dog_file_obs.observe(state) == {"health_status": 1}
assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)}) assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)})
# TODO:
# def test_file_num_access():
# ...

View File

@@ -163,7 +163,7 @@ def test_restore_backup_without_updating_scan(uc2_network):
db_service.db_file.corrupt() # corrupt the db db_service.db_file.corrupt() # corrupt the db
assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT # db file is actually corrupt assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT # db file is actually corrupt
assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.GOOD # not scanned yet assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.NONE # not scanned yet
db_service.db_file.scan() # scan the db file db_service.db_file.scan() # scan the db file
@@ -190,7 +190,7 @@ def test_restore_backup_after_deleting_file_without_updating_scan(uc2_network):
db_service.db_file.corrupt() # corrupt the db db_service.db_file.corrupt() # corrupt the db
assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT # db file is actually corrupt assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT # db file is actually corrupt
assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.GOOD # not scanned yet assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.NONE # not scanned yet
db_service.db_file.scan() # scan the db file db_service.db_file.scan() # scan the db file

View File

@@ -69,8 +69,8 @@ class TestFileSystemRequiresScan:
wildcard_list: wildcard_list:
- 0.0.0.1 - 0.0.0.1
port_list: port_list:
- 80 - HTTP
- 5432 - POSTGRES_SERVER
protocol_list: protocol_list:
- ICMP - ICMP
- TCP - TCP
@@ -119,14 +119,20 @@ class TestFileSystemRequiresScan:
assert obs_not_requiring_scan.observe(file_state)["health_status"] == 3 assert obs_not_requiring_scan.observe(file_state)["health_status"] == 3
def test_folder_require_scan(self): def test_folder_require_scan(self):
folder_state = {"health_status": 3, "visible_status": 1} folder_state = {"health_status": 3, "visible_status": 1, "scanned_this_step": False}
obs_requiring_scan = FolderObservation( obs_requiring_scan = FolderObservation(
[], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True
) )
assert obs_requiring_scan.observe(folder_state)["health_status"] == 1 assert obs_requiring_scan.observe(folder_state)["health_status"] == 0
obs_not_requiring_scan = FolderObservation( obs_not_requiring_scan = FolderObservation(
[], files=[], num_files=0, include_num_access=False, file_system_requires_scan=False [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=False
) )
assert obs_not_requiring_scan.observe(folder_state)["health_status"] == 3 assert obs_not_requiring_scan.observe(folder_state)["health_status"] == 3
folder_state = {"health_status": 3, "visible_status": 1, "scanned_this_step": True}
obs_requiring_scan = FolderObservation(
[], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True
)
assert obs_requiring_scan.observe(folder_state)["health_status"] == 1

View File

@@ -22,12 +22,12 @@ def test_file_scan(file_system):
file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder") file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder")
assert file.health_status == FileSystemItemHealthStatus.GOOD assert file.health_status == FileSystemItemHealthStatus.GOOD
assert file.visible_health_status == FileSystemItemHealthStatus.GOOD assert file.visible_health_status == FileSystemItemHealthStatus.NONE
file.corrupt() file.corrupt()
assert file.health_status == FileSystemItemHealthStatus.CORRUPT assert file.health_status == FileSystemItemHealthStatus.CORRUPT
assert file.visible_health_status == FileSystemItemHealthStatus.GOOD assert file.visible_health_status == FileSystemItemHealthStatus.NONE
file.scan() file.scan()

View File

@@ -24,7 +24,7 @@ def test_file_scan_request(populated_file_system):
file.corrupt() file.corrupt()
assert file.health_status == FileSystemItemHealthStatus.CORRUPT assert file.health_status == FileSystemItemHealthStatus.CORRUPT
assert file.visible_health_status == FileSystemItemHealthStatus.GOOD assert file.visible_health_status == FileSystemItemHealthStatus.NONE
fs.apply_request(request=["folder", folder.name, "file", file.name, "scan"]) fs.apply_request(request=["folder", folder.name, "file", file.name, "scan"])
@@ -94,7 +94,7 @@ def test_deleted_file_cannot_be_interacted_with(populated_file_system):
assert fs.get_file(folder_name=folder.name, file_name=file.name).health_status == FileSystemItemHealthStatus.CORRUPT assert fs.get_file(folder_name=folder.name, file_name=file.name).health_status == FileSystemItemHealthStatus.CORRUPT
assert ( assert (
fs.get_file(folder_name=folder.name, file_name=file.name).visible_health_status fs.get_file(folder_name=folder.name, file_name=file.name).visible_health_status
== FileSystemItemHealthStatus.GOOD == FileSystemItemHealthStatus.NONE
) )
fs.apply_request(request=["delete", "file", folder.name, file.name]) fs.apply_request(request=["delete", "file", folder.name, file.name])

View File

@@ -44,25 +44,25 @@ def test_folder_scan(file_system):
file2: File = folder.get_file_by_id(file_uuid=list(folder.files)[0]) file2: File = folder.get_file_by_id(file_uuid=list(folder.files)[0])
assert folder.health_status == FileSystemItemHealthStatus.GOOD assert folder.health_status == FileSystemItemHealthStatus.GOOD
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file1.visible_health_status == FileSystemItemHealthStatus.NONE
assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.NONE
folder.corrupt() folder.corrupt()
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file1.visible_health_status == FileSystemItemHealthStatus.NONE
assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.NONE
folder.scan() folder.scan()
folder.apply_timestep(timestep=0) folder.apply_timestep(timestep=0)
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file1.visible_health_status == FileSystemItemHealthStatus.NONE
assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.NONE
folder.apply_timestep(timestep=1) folder.apply_timestep(timestep=1)
folder.apply_timestep(timestep=2) folder.apply_timestep(timestep=2)

View File

@@ -29,18 +29,18 @@ def test_folder_scan_request(populated_file_system):
folder.corrupt() folder.corrupt()
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file1.visible_health_status == FileSystemItemHealthStatus.NONE
assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.NONE
fs.apply_request(request=["folder", folder.name, "scan"]) fs.apply_request(request=["folder", folder.name, "scan"])
folder.apply_timestep(timestep=0) folder.apply_timestep(timestep=0)
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD assert file1.visible_health_status == FileSystemItemHealthStatus.NONE
assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD assert file2.visible_health_status == FileSystemItemHealthStatus.NONE
folder.apply_timestep(timestep=1) folder.apply_timestep(timestep=1)
folder.apply_timestep(timestep=2) folder.apply_timestep(timestep=2)

View File

@@ -70,13 +70,13 @@ def test_node_os_scan(node):
# add folder and file to node # add folder and file to node
folder: Folder = node.file_system.create_folder(folder_name="test_folder") folder: Folder = node.file_system.create_folder(folder_name="test_folder")
folder.corrupt() folder.corrupt()
assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD assert folder.visible_health_status == FileSystemItemHealthStatus.NONE
file: File = node.file_system.create_file(folder_name="test_folder", file_name="file.txt") 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") file2: File = node.file_system.create_file(folder_name="test_folder", file_name="file2.txt")
file.corrupt() file.corrupt()
file2.corrupt() file2.corrupt()
assert file.visible_health_status == FileSystemItemHealthStatus.GOOD assert file.visible_health_status == FileSystemItemHealthStatus.NONE
# run os scan # run os scan
node.apply_request(["os", "scan"]) node.apply_request(["os", "scan"])