From 4796cee2dc7882a592801ece308d6eeebc82ba60 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 26 Jun 2024 16:51:30 +0100 Subject: [PATCH 01/16] #2676: Put global variables in dataclass --- src/primaite/simulator/network/nmne.py | 81 ++++++++++++++------------ 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index 5c0c657b..d6f1763f 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -1,48 +1,55 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, Final, List - -CAPTURE_NMNE: bool = True -"""Indicates whether Malicious Network Events (MNEs) should be captured. Default is True.""" - -NMNE_CAPTURE_KEYWORDS: List[str] = [] -"""List of keywords to identify malicious network events.""" - -# TODO: Remove final and make configurable after example layout when the NICObservation creates nmne structure dynamically -CAPTURE_BY_DIRECTION: Final[bool] = True -"""Flag to determine if captures should be organized by traffic direction (inbound/outbound).""" -CAPTURE_BY_IP_ADDRESS: Final[bool] = False -"""Flag to determine if captures should be organized by source or destination IP address.""" -CAPTURE_BY_PROTOCOL: Final[bool] = False -"""Flag to determine if captures should be organized by network protocol (e.g., TCP, UDP).""" -CAPTURE_BY_PORT: Final[bool] = False -"""Flag to determine if captures should be organized by source or destination port.""" -CAPTURE_BY_KEYWORD: Final[bool] = False -"""Flag to determine if captures should be filtered and categorised based on specific keywords.""" +from dataclasses import dataclass, field +from typing import Dict, List -def set_nmne_config(nmne_config: Dict): +@dataclass +class nmne_data: + """Store all the information to perform NMNE operations.""" + + capture_nmne: bool = True + """Indicates whether Malicious Network Events (MNEs) should be captured.""" + nmne_capture_keywords: List[str] = field(default_factory=list) + """List of keywords to identify malicious network events.""" + capture_by_direction: bool = True + """Captures should be organized by traffic direction (inbound/outbound).""" + capture_by_ip_address: bool = False + """Captures should be organized by source or destination IP address.""" + capture_by_protocol: bool = False + """Captures should be organized by network protocol (e.g., TCP, UDP).""" + capture_by_port: bool = False + """Captures should be organized by source or destination port.""" + capture_by_keyword: bool = False + """Captures should be filtered and categorised based on specific keywords.""" + + +def set_nmne_config(nmne_config: Dict) -> nmne_data: """ - Sets the configuration for capturing Malicious Network Events (MNEs) based on a provided dictionary. + Sets the configuration for capturing Malicious Network Events (MNEs) based on a provided + dictionary. - This function updates global settings related to NMNE capture, including whether to capture NMNEs and what - keywords to use for identifying NMNEs. + This function updates global settings related to NMNE capture, including whether to capture + NMNEs and what keywords to use for identifying NMNEs. - The function ensures that the settings are updated only if they are provided in the `nmne_config` dictionary, - and maintains type integrity by checking the types of the provided values. + The function ensures that the settings are updated only if they are provided in the + `nmne_config` dictionary, and maintains type integrity by checking the types of the provided + values. - :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys include: - "capture_nmne" (bool) to indicate whether NMNEs should be captured, "nmne_capture_keywords" (list of strings) - to specify keywords for NMNE identification. + :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys + include: + "capture_nmne" (bool) to indicate whether NMNEs should be captured; + "nmne_capture_keywords" (list of strings) to specify keywords for NMNE identification. + :rvar dataclass with data read from config file. """ - global NMNE_CAPTURE_KEYWORDS - global CAPTURE_NMNE - + nmne_capture_keywords = [] # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect - CAPTURE_NMNE = nmne_config.get("capture_nmne", False) - if not isinstance(CAPTURE_NMNE, bool): - CAPTURE_NMNE = True # Revert to default True if the provided value is not a boolean + capture_nmne = nmne_config.get("capture_nmne", False) + if not isinstance(capture_nmne, bool): + capture_nmne = True # Revert to default True if the provided value is not a boolean # Update the NMNE capture keywords, appending new keywords if provided - NMNE_CAPTURE_KEYWORDS += nmne_config.get("nmne_capture_keywords", []) - if not isinstance(NMNE_CAPTURE_KEYWORDS, list): - NMNE_CAPTURE_KEYWORDS = [] # Reset to empty list if the provided value is not a list + nmne_capture_keywords += nmne_config.get("nmne_capture_keywords", []) + if not isinstance(nmne_capture_keywords, list): + nmne_capture_keywords = [] # Reset to empty list if the provided value is not a list + + return nmne_data(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) From dbc1d73c34f31a3d01e38471ddaa88834a7dfe63 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 2 Jul 2024 11:15:31 +0100 Subject: [PATCH 02/16] #2676: Update naming of NMNE class --- src/primaite/game/game.py | 13 ++++++++++--- src/primaite/simulator/network/nmne.py | 11 +++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8a79d068..cc559b4d 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -23,7 +23,7 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.nmne import set_nmne_config +from primaite.simulator.network.nmne import store_nmne_config, NmneData from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient @@ -113,6 +113,9 @@ class PrimaiteGame: self._reward_calculation_order: List[str] = [name for name in self.agents] """Agent order for reward evaluation, as some rewards can be dependent on other agents' rewards.""" + self.nmne_config: NmneData = None + """ Config data from Number of Malicious Network Events.""" + def step(self): """ Perform one step of the simulation/agent loop. @@ -496,10 +499,11 @@ class PrimaiteGame: # Validate that if any agents are sharing rewards, they aren't forming an infinite loop. game.setup_reward_sharing() - # Set the NMNE capture config - set_nmne_config(network_config.get("nmne_config", {})) game.update_agents(game.get_sim_state()) + # Set the NMNE capture config + game.nmne_config = store_nmne_config(network_config.get("nmne_config", {})) + return game def setup_reward_sharing(self): @@ -539,3 +543,6 @@ class PrimaiteGame: # sort the agents so the rewards that depend on other rewards are always evaluated later self._reward_calculation_order = topological_sort(graph) + + def get_nmne_config(self) -> NmneData: + return self.nmne_config diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index d6f1763f..947f27ac 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -4,7 +4,7 @@ from typing import Dict, List @dataclass -class nmne_data: +class NmneData: """Store all the information to perform NMNE operations.""" capture_nmne: bool = True @@ -23,10 +23,9 @@ class nmne_data: """Captures should be filtered and categorised based on specific keywords.""" -def set_nmne_config(nmne_config: Dict) -> nmne_data: +def store_nmne_config(nmne_config: Dict) -> NmneData: """ - Sets the configuration for capturing Malicious Network Events (MNEs) based on a provided - dictionary. + Store configuration for capturing Malicious Network Events (MNEs). This function updates global settings related to NMNE capture, including whether to capture NMNEs and what keywords to use for identifying NMNEs. @@ -41,7 +40,7 @@ def set_nmne_config(nmne_config: Dict) -> nmne_data: "nmne_capture_keywords" (list of strings) to specify keywords for NMNE identification. :rvar dataclass with data read from config file. """ - nmne_capture_keywords = [] + nmne_capture_keywords: List[str] = [] # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect capture_nmne = nmne_config.get("capture_nmne", False) if not isinstance(capture_nmne, bool): @@ -52,4 +51,4 @@ def set_nmne_config(nmne_config: Dict) -> nmne_data: if not isinstance(nmne_capture_keywords, list): nmne_capture_keywords = [] # Reset to empty list if the provided value is not a list - return nmne_data(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) + return NmneData(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) From 47df2aa56940c26047c4e2b6672867e9016c8b1f Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 4 Jul 2024 15:41:13 +0100 Subject: [PATCH 03/16] #2676: Store NMNE config data in class variable. --- src/primaite/game/game.py | 13 ++---- .../simulator/network/hardware/base.py | 42 +++++++------------ 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index cc559b4d..9636bd23 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -15,7 +15,7 @@ from primaite.game.agent.scripted_agents.probabilistic_agent import Probabilisti from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort -from primaite.simulator.network.hardware.base import NodeOperatingState +from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server @@ -23,7 +23,7 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.nmne import store_nmne_config, NmneData +from primaite.simulator.network.nmne import NmneData, store_nmne_config from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient @@ -239,6 +239,8 @@ class PrimaiteGame: nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) + # Set the NMNE capture config + NetworkInterface.nmne_config = store_nmne_config(network_config.get("nmne_config", {})) for node_cfg in nodes_cfg: n_type = node_cfg["type"] @@ -500,10 +502,6 @@ class PrimaiteGame: game.setup_reward_sharing() game.update_agents(game.get_sim_state()) - - # Set the NMNE capture config - game.nmne_config = store_nmne_config(network_config.get("nmne_config", {})) - return game def setup_reward_sharing(self): @@ -543,6 +541,3 @@ class PrimaiteGame: # sort the agents so the rewards that depend on other rewards are always evaluated later self._reward_calculation_order = topological_sort(graph) - - def get_nmne_config(self) -> NmneData: - return self.nmne_config diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 01745215..6d753731 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -6,12 +6,11 @@ import secrets from abc import ABC, abstractmethod from ipaddress import IPv4Address, IPv4Network from pathlib import Path -from typing import Any, Dict, Optional, Type, TypeVar, Union +from typing import Any, ClassVar, Dict, Optional, Type, TypeVar, Union from prettytable import MARKDOWN, PrettyTable from pydantic import BaseModel, Field -import primaite.simulator.network.nmne from primaite import getLogger from primaite.exceptions import NetworkError from primaite.interface.request import RequestResponse @@ -20,15 +19,7 @@ from primaite.simulator.core import RequestFormat, RequestManager, RequestPermis from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.nmne import ( - CAPTURE_BY_DIRECTION, - CAPTURE_BY_IP_ADDRESS, - CAPTURE_BY_KEYWORD, - CAPTURE_BY_PORT, - CAPTURE_BY_PROTOCOL, - CAPTURE_NMNE, - NMNE_CAPTURE_KEYWORDS, -) +from primaite.simulator.network.nmne import NmneData from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.system.applications.application import Application @@ -108,8 +99,8 @@ class NetworkInterface(SimComponent, ABC): pcap: Optional[PacketCapture] = None "A PacketCapture instance for capturing and analysing packets passing through this interface." - nmne: Dict = Field(default_factory=lambda: {}) - "A dict containing details of the number of malicious network events captured." + nmne_config: ClassVar[NmneData] = None + "A dataclass defining malicious network events to be captured." traffic: Dict = Field(default_factory=lambda: {}) "A dict containing details of the inbound and outbound traffic by port and protocol." @@ -117,7 +108,6 @@ class NetworkInterface(SimComponent, ABC): def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" super().setup_for_episode(episode=episode) - self.nmne = {} self.traffic = {} if episode and self.pcap and SIM_OUTPUT.save_pcap_logs: self.pcap.current_episode = episode @@ -152,8 +142,8 @@ class NetworkInterface(SimComponent, ABC): "enabled": self.enabled, } ) - if CAPTURE_NMNE: - state.update({"nmne": {k: v for k, v in self.nmne.items()}}) + if self.nmne_config and self.nmne_config.capture_nmne: + state.update({"nmne": {self.nmne_config.__dict__}}) state.update({"traffic": convert_dict_enum_keys_to_enum_values(self.traffic)}) return state @@ -186,7 +176,7 @@ class NetworkInterface(SimComponent, ABC): :param inbound: Boolean indicating if the frame direction is inbound. Defaults to True. """ # Exit function if NMNE capturing is disabled - if not CAPTURE_NMNE: + if not (self.nmne_config and self.nmne_config.capture_nmne): return # Initialise basic frame data variables @@ -207,27 +197,27 @@ class NetworkInterface(SimComponent, ABC): frame_str = str(frame.payload) # Proceed only if any NMNE keyword is present in the frame payload - if any(keyword in frame_str for keyword in NMNE_CAPTURE_KEYWORDS): + if any(keyword in frame_str for keyword in self.nmne_config.nmne_capture_keywords): # Start with the root of the NMNE capture structure - current_level = self.nmne + current_level = self.nmne_config # Update NMNE structure based on enabled settings - if CAPTURE_BY_DIRECTION: + if self.nmne_config.capture_by_direction: # Set or get the dictionary for the current direction current_level = current_level.setdefault("direction", {}) current_level = current_level.setdefault(direction, {}) - if CAPTURE_BY_IP_ADDRESS: + if self.nmne_config.capture_by_ip_address: # Set or get the dictionary for the current IP address current_level = current_level.setdefault("ip_address", {}) current_level = current_level.setdefault(ip_address, {}) - if CAPTURE_BY_PROTOCOL: + if self.nmne_config.capture_by_protocol: # Set or get the dictionary for the current protocol current_level = current_level.setdefault("protocol", {}) current_level = current_level.setdefault(protocol, {}) - if CAPTURE_BY_PORT: + if self.nmne_config.capture_by_port: # Set or get the dictionary for the current port current_level = current_level.setdefault("port", {}) current_level = current_level.setdefault(port, {}) @@ -236,8 +226,8 @@ class NetworkInterface(SimComponent, ABC): keyword_level = current_level.setdefault("keywords", {}) # Increment the count for detected keywords in the payload - if CAPTURE_BY_KEYWORD: - for keyword in NMNE_CAPTURE_KEYWORDS: + if self.nmne_config.capture_by_keyword: + for keyword in self.nmne_config.nmne_capture_keywords: if keyword in frame_str: # Update the count for each keyword found keyword_level[keyword] = keyword_level.get(keyword, 0) + 1 @@ -1067,7 +1057,7 @@ class Node(SimComponent): ip_address, network_interface.speed, "Enabled" if network_interface.enabled else "Disabled", - network_interface.nmne if primaite.simulator.network.nmne.CAPTURE_NMNE else "Disabled", + network_interface.nmne if self.nmne_config.capture_nmne else "Disabled", ] ) print(table) From 3867ec40c9c571d719ff6fe818c505721a99cbc0 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 4 Jul 2024 17:05:00 +0100 Subject: [PATCH 04/16] #2676: Fix nmne_config dict conversion --- src/primaite/simulator/network/hardware/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 6d753731..3c52a65d 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1,6 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations +from dataclasses import asdict import re import secrets from abc import ABC, abstractmethod @@ -143,7 +144,7 @@ class NetworkInterface(SimComponent, ABC): } ) if self.nmne_config and self.nmne_config.capture_nmne: - state.update({"nmne": {self.nmne_config.__dict__}}) + state.update({"nmne": asdict(self.nmne_config)}) state.update({"traffic": convert_dict_enum_keys_to_enum_values(self.traffic)}) return state From 589ea2fed4e96e14365a4f92c504ad7fbf21b6c2 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 5 Jul 2024 12:19:52 +0100 Subject: [PATCH 05/16] #2676: Add local nmne dict --- src/primaite/simulator/network/hardware/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 3c52a65d..e611f9b2 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -103,12 +103,16 @@ class NetworkInterface(SimComponent, ABC): nmne_config: ClassVar[NmneData] = None "A dataclass defining malicious network events to be captured." + nmne: Dict = Field(default_factory=lambda: {}) + "A dict containing details of the number of malicious events captured." + traffic: Dict = Field(default_factory=lambda: {}) "A dict containing details of the inbound and outbound traffic by port and protocol." def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" super().setup_for_episode(episode=episode) + self.nmne = {} self.traffic = {} if episode and self.pcap and SIM_OUTPUT.save_pcap_logs: self.pcap.current_episode = episode @@ -144,7 +148,7 @@ class NetworkInterface(SimComponent, ABC): } ) if self.nmne_config and self.nmne_config.capture_nmne: - state.update({"nmne": asdict(self.nmne_config)}) + state.update({"nmne": self.nmne}) state.update({"traffic": convert_dict_enum_keys_to_enum_values(self.traffic)}) return state @@ -200,7 +204,7 @@ class NetworkInterface(SimComponent, ABC): # Proceed only if any NMNE keyword is present in the frame payload if any(keyword in frame_str for keyword in self.nmne_config.nmne_capture_keywords): # Start with the root of the NMNE capture structure - current_level = self.nmne_config + current_level = self.nmne # Update NMNE structure based on enabled settings if self.nmne_config.capture_by_direction: From 18ae3acf3734f36a86cf46045ef5f0ed5c51e02c Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 5 Jul 2024 14:09:39 +0100 Subject: [PATCH 06/16] #2676: Update nmne tests --- src/primaite/simulator/network/hardware/base.py | 1 - .../network/test_capture_nmne.py | 16 +++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index e611f9b2..f161b2b5 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1,7 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations -from dataclasses import asdict import re import secrets from abc import ABC, abstractmethod diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index a8f1f245..f6e4c685 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,12 +1,14 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.nmne import set_nmne_config +from primaite.simulator.network.nmne import store_nmne_config from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection -def test_capture_nmne(uc2_network): +def test_capture_nmne(uc2_network: Network): """ Conducts a test to verify that Malicious Network Events (MNEs) are correctly captured. @@ -33,7 +35,7 @@ def test_capture_nmne(uc2_network): } # Apply the NMNE configuration settings - set_nmne_config(nmne_config) + NIC.nmne_config = store_nmne_config(nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers assert web_server_nic.nmne == {} @@ -82,7 +84,7 @@ def test_capture_nmne(uc2_network): assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 3}}}} -def test_describe_state_nmne(uc2_network): +def test_describe_state_nmne(uc2_network: Network): """ Conducts a test to verify that Malicious Network Events (MNEs) are correctly represented in the nic state. @@ -110,7 +112,7 @@ def test_describe_state_nmne(uc2_network): } # Apply the NMNE configuration settings - set_nmne_config(nmne_config) + NIC.nmne_config = store_nmne_config(nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers web_server_nic_state = web_server_nic.describe_state() @@ -190,7 +192,7 @@ def test_describe_state_nmne(uc2_network): assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 4}}}} -def test_capture_nmne_observations(uc2_network): +def test_capture_nmne_observations(uc2_network: Network): """ Tests the NICObservation class's functionality within a simulated network environment. @@ -219,7 +221,7 @@ def test_capture_nmne_observations(uc2_network): } # Apply the NMNE configuration settings - set_nmne_config(nmne_config) + NIC.nmne_config = store_nmne_config(nmne_config) # Define observations for the NICs of the database and web servers db_server_nic_obs = NICObservation(where=["network", "nodes", "database_server", "NICs", 1], include_nmne=True) From 34969c588b6a93134d77ecc518b64bed1988437d Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 16 Jul 2024 08:59:36 +0100 Subject: [PATCH 07/16] #2676: Fix mismerge. --- src/primaite/game/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c2a1961b..3e129879 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -18,7 +18,7 @@ from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency -from primaite.simulator.network.hardware.base import NodeOperatingState +from primaite.simulator.network.hardware.base import NodeOperatingState, NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server From 07e736977ccefbe567cab88aed0c023c012c92d6 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 16 Jul 2024 16:58:11 +0100 Subject: [PATCH 08/16] #2676: Fix some more integration tests --- tests/assets/configs/bad_primaite_session.yaml | 2 +- tests/assets/configs/basic_switched_network.yaml | 2 +- tests/assets/configs/eval_only_primaite_session.yaml | 2 +- tests/assets/configs/firewall_actions_network.yaml | 2 +- tests/assets/configs/fix_duration_one_item.yaml | 2 +- tests/assets/configs/software_fix_duration.yaml | 2 +- tests/assets/configs/test_primaite_session.yaml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 8cbd3ae9..c83cadc8 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -99,7 +99,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 69187fa3..fed0f52d 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -92,7 +92,7 @@ agents: - NONE tcp: - DNS - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index de861dcc..3d60eb6e 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -111,7 +111,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index fd5b1bf8..2292616d 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -68,7 +68,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index 59bc15f9..bd0fb61f 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -89,7 +89,7 @@ agents: - NONE tcp: - DNS - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 1acb05a9..1a28258b 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -89,7 +89,7 @@ agents: - NONE tcp: - DNS - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index eb8103e8..27cfa240 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -120,7 +120,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 From 061509dffdd5d7f7fa4088bce2b082b487567f3b Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 17 Jul 2024 10:43:04 +0100 Subject: [PATCH 09/16] #2676: Further test fixes. --- src/primaite/game/game.py | 2 +- .../scenario_with_placeholders/scenario.yaml | 2 +- .../observations/test_nic_observations.py | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 3e129879..aca75b63 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -18,7 +18,7 @@ from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency -from primaite.simulator.network.hardware.base import NodeOperatingState, NetworkInterface +from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index 81848b2d..ef930a1a 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -44,7 +44,7 @@ agents: num_files: 1 num_nics: 1 include_num_access: false - include_nmne: true + include_nmne: false - type: LINKS label: LINKS diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 88dd2bd5..dfad8b59 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -9,9 +9,11 @@ from gymnasium import spaces from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.nmne import store_nmne_config from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser @@ -75,6 +77,18 @@ def test_nic(simulation): nic_obs = NICObservation(where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=True) + # Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs + nmne_config = { + "capture_nmne": True, # Enable the capture of MNEs + "nmne_capture_keywords": [ + "DELETE", + "ENCRYPT", + ], # Specify "DELETE/ENCRYPT" SQL command as a keyword for MNE detection + } + + # Apply the NMNE configuration settings + NetworkInterface.nmne_config = store_nmne_config(nmne_config) + assert nic_obs.space["nic_status"] == spaces.Discrete(3) assert nic_obs.space["NMNE"]["inbound"] == spaces.Discrete(4) assert nic_obs.space["NMNE"]["outbound"] == spaces.Discrete(4) @@ -144,7 +158,7 @@ def test_nic_monitored_traffic(simulation): pc2: Computer = simulation.network.get_node_by_hostname("client_2") nic_obs = NICObservation( - where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=True, monitored_traffic=monitored_traffic + where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=False, monitored_traffic=monitored_traffic ) simulation.pre_timestep(0) # apply timestep to whole sim From 43617340148051973518f72b45dce01cbb6f40cf Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 17 Jul 2024 17:50:55 +0100 Subject: [PATCH 10/16] #2676: Code review changes --- src/primaite/game/game.py | 5 +---- src/primaite/simulator/network/hardware/base.py | 4 ++-- src/primaite/simulator/network/nmne.py | 11 +++++------ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index aca75b63..0c1b3192 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -26,7 +26,7 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.nmne import NmneData, store_nmne_config +from primaite.simulator.network.nmne import store_nmne_config from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application @@ -110,9 +110,6 @@ class PrimaiteGame: self._reward_calculation_order: List[str] = [name for name in self.agents] """Agent order for reward evaluation, as some rewards can be dependent on other agents' rewards.""" - self.nmne_config: NmneData = None - """ Config data from Number of Malicious Network Events.""" - def step(self): """ Perform one step of the simulation/agent loop. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index f85d3f2e..50549389 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -19,7 +19,7 @@ from primaite.simulator.core import RequestFormat, RequestManager, RequestPermis from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.nmne import NmneData +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.system.applications.application import Application @@ -99,7 +99,7 @@ class NetworkInterface(SimComponent, ABC): pcap: Optional[PacketCapture] = None "A PacketCapture instance for capturing and analysing packets passing through this interface." - nmne_config: ClassVar[NmneData] = None + nmne_config: ClassVar[NMNEConfig] = None "A dataclass defining malicious network events to be captured." nmne: Dict = Field(default_factory=lambda: {}) diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index 947f27ac..431ec07d 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -1,15 +1,14 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from dataclasses import dataclass, field +from pydantic import BaseModel from typing import Dict, List -@dataclass -class NmneData: +class NMNEConfig(BaseModel): """Store all the information to perform NMNE operations.""" capture_nmne: bool = True """Indicates whether Malicious Network Events (MNEs) should be captured.""" - nmne_capture_keywords: List[str] = field(default_factory=list) + nmne_capture_keywords: List[str] = [] """List of keywords to identify malicious network events.""" capture_by_direction: bool = True """Captures should be organized by traffic direction (inbound/outbound).""" @@ -23,7 +22,7 @@ class NmneData: """Captures should be filtered and categorised based on specific keywords.""" -def store_nmne_config(nmne_config: Dict) -> NmneData: +def store_nmne_config(nmne_config: Dict) -> NMNEConfig: """ Store configuration for capturing Malicious Network Events (MNEs). @@ -51,4 +50,4 @@ def store_nmne_config(nmne_config: Dict) -> NmneData: if not isinstance(nmne_capture_keywords, list): nmne_capture_keywords = [] # Reset to empty list if the provided value is not a list - return NmneData(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) + return NMNEConfig(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) From 9fb3790c1a731779e368207cc02b1fe1f587b5de Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 19 Jul 2024 11:10:57 +0100 Subject: [PATCH 11/16] #2726: Resolve pydantic validators PR comment --- src/primaite/simulator/network/nmne.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index 431ec07d..c9266fba 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -6,7 +6,7 @@ from typing import Dict, List class NMNEConfig(BaseModel): """Store all the information to perform NMNE operations.""" - capture_nmne: bool = True + capture_nmne: bool = False """Indicates whether Malicious Network Events (MNEs) should be captured.""" nmne_capture_keywords: List[str] = [] """List of keywords to identify malicious network events.""" @@ -42,12 +42,8 @@ def store_nmne_config(nmne_config: Dict) -> NMNEConfig: nmne_capture_keywords: List[str] = [] # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect capture_nmne = nmne_config.get("capture_nmne", False) - if not isinstance(capture_nmne, bool): - capture_nmne = True # Revert to default True if the provided value is not a boolean # Update the NMNE capture keywords, appending new keywords if provided nmne_capture_keywords += nmne_config.get("nmne_capture_keywords", []) - if not isinstance(nmne_capture_keywords, list): - nmne_capture_keywords = [] # Reset to empty list if the provided value is not a list return NMNEConfig(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) From e4ade6ba5484f70d2b9f1e5917513d4d698823eb Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 19 Jul 2024 12:02:43 +0100 Subject: [PATCH 12/16] #2676: Merge nmne.py with io.py --- src/primaite/game/game.py | 2 +- src/primaite/session/io.py | 46 +++++++++++++++++ .../simulator/network/hardware/base.py | 2 +- src/primaite/simulator/network/nmne.py | 49 ------------------- .../observations/test_nic_observations.py | 2 +- .../network/test_capture_nmne.py | 2 +- 6 files changed, 50 insertions(+), 53 deletions(-) delete mode 100644 src/primaite/simulator/network/nmne.py diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 0c1b3192..cd0180db 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -16,6 +16,7 @@ from primaite.game.agent.scripted_agents.probabilistic_agent import Probabilisti from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort +from primaite.session.io import store_nmne_config from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState @@ -26,7 +27,6 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.nmne import store_nmne_config from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 78d7cb3c..2d0d5897 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -131,3 +131,49 @@ class PrimaiteIO: new = cls(settings=cls.Settings(**config)) return new + + +class NMNEConfig(BaseModel): + """Store all the information to perform NMNE operations.""" + + capture_nmne: bool = False + """Indicates whether Malicious Network Events (MNEs) should be captured.""" + nmne_capture_keywords: List[str] = [] + """List of keywords to identify malicious network events.""" + capture_by_direction: bool = True + """Captures should be organized by traffic direction (inbound/outbound).""" + capture_by_ip_address: bool = False + """Captures should be organized by source or destination IP address.""" + capture_by_protocol: bool = False + """Captures should be organized by network protocol (e.g., TCP, UDP).""" + capture_by_port: bool = False + """Captures should be organized by source or destination port.""" + capture_by_keyword: bool = False + """Captures should be filtered and categorised based on specific keywords.""" + + +def store_nmne_config(nmne_config: Dict) -> NMNEConfig: + """ + Store configuration for capturing Malicious Network Events (MNEs). + + This function updates global settings related to NMNE capture, including whether to capture + NMNEs and what keywords to use for identifying NMNEs. + + The function ensures that the settings are updated only if they are provided in the + `nmne_config` dictionary, and maintains type integrity by checking the types of the provided + values. + + :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys + include: + "capture_nmne" (bool) to indicate whether NMNEs should be captured; + "nmne_capture_keywords" (list of strings) to specify keywords for NMNE identification. + :rvar dataclass with data read from config file. + """ + nmne_capture_keywords: List[str] = [] + # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect + capture_nmne = nmne_config.get("capture_nmne", False) + + # Update the NMNE capture keywords, appending new keywords if provided + nmne_capture_keywords += nmne_config.get("nmne_capture_keywords", []) + + return NMNEConfig(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 50549389..aafdbe5c 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -14,12 +14,12 @@ from pydantic import BaseModel, Field from primaite import getLogger from primaite.exceptions import NetworkError from primaite.interface.request import RequestResponse +from primaite.session.io import NMNEConfig from primaite.simulator import SIM_OUTPUT from primaite.simulator.core import RequestFormat, RequestManager, RequestPermissionValidator, RequestType, SimComponent from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.system.applications.application import Application diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py deleted file mode 100644 index c9266fba..00000000 --- a/src/primaite/simulator/network/nmne.py +++ /dev/null @@ -1,49 +0,0 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from pydantic import BaseModel -from typing import Dict, List - - -class NMNEConfig(BaseModel): - """Store all the information to perform NMNE operations.""" - - capture_nmne: bool = False - """Indicates whether Malicious Network Events (MNEs) should be captured.""" - nmne_capture_keywords: List[str] = [] - """List of keywords to identify malicious network events.""" - capture_by_direction: bool = True - """Captures should be organized by traffic direction (inbound/outbound).""" - capture_by_ip_address: bool = False - """Captures should be organized by source or destination IP address.""" - capture_by_protocol: bool = False - """Captures should be organized by network protocol (e.g., TCP, UDP).""" - capture_by_port: bool = False - """Captures should be organized by source or destination port.""" - capture_by_keyword: bool = False - """Captures should be filtered and categorised based on specific keywords.""" - - -def store_nmne_config(nmne_config: Dict) -> NMNEConfig: - """ - Store configuration for capturing Malicious Network Events (MNEs). - - This function updates global settings related to NMNE capture, including whether to capture - NMNEs and what keywords to use for identifying NMNEs. - - The function ensures that the settings are updated only if they are provided in the - `nmne_config` dictionary, and maintains type integrity by checking the types of the provided - values. - - :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys - include: - "capture_nmne" (bool) to indicate whether NMNEs should be captured; - "nmne_capture_keywords" (list of strings) to specify keywords for NMNE identification. - :rvar dataclass with data read from config file. - """ - nmne_capture_keywords: List[str] = [] - # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect - capture_nmne = nmne_config.get("capture_nmne", False) - - # Update the NMNE capture keywords, appending new keywords if provided - nmne_capture_keywords += nmne_config.get("nmne_capture_keywords", []) - - return NMNEConfig(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index dfad8b59..7f86d26d 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -9,11 +9,11 @@ from gymnasium import spaces from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.game import PrimaiteGame +from primaite.session.io import store_nmne_config from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.nmne import store_nmne_config from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index f6e4c685..b4162e58 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,9 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation +from primaite.session.io import store_nmne_config from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.nmne import store_nmne_config from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection From 82a11b8b85c28bf90bbac3169f196b09fbfb8c4b Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 19 Jul 2024 12:54:01 +0100 Subject: [PATCH 13/16] #2676: Updated doc strings --- src/primaite/session/io.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 2d0d5897..c634e835 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -156,18 +156,17 @@ def store_nmne_config(nmne_config: Dict) -> NMNEConfig: """ Store configuration for capturing Malicious Network Events (MNEs). - This function updates global settings related to NMNE capture, including whether to capture - NMNEs and what keywords to use for identifying NMNEs. + This function updates settings related to NMNE capture, stored in NMNEConfig including whether + to capture NMNEs and the keywords to use for identifying NMNEs. The function ensures that the settings are updated only if they are provided in the - `nmne_config` dictionary, and maintains type integrity by checking the types of the provided - values. + `nmne_config` dictionary, and maintains type integrity by relying on pydantic validators. :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys include: "capture_nmne" (bool) to indicate whether NMNEs should be captured; "nmne_capture_keywords" (list of strings) to specify keywords for NMNE identification. - :rvar dataclass with data read from config file. + :rvar class with data read from config file. """ nmne_capture_keywords: List[str] = [] # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect From 9bf8d0f8cbce18542622bf772fd9abb1edf50bc6 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 31 Jul 2024 13:20:15 +0100 Subject: [PATCH 14/16] #2676 Put NMNE back into network module --- src/primaite/game/game.py | 4 +- src/primaite/session/io.py | 45 ------------------- .../simulator/network/hardware/base.py | 2 +- src/primaite/simulator/network/nmne.py | 25 +++++++++++ .../observations/test_nic_observations.py | 4 +- .../network/test_capture_nmne.py | 8 ++-- 6 files changed, 34 insertions(+), 54 deletions(-) create mode 100644 src/primaite/simulator/network/nmne.py diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index cd0180db..2e7ee735 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -16,7 +16,6 @@ from primaite.game.agent.scripted_agents.probabilistic_agent import Probabilisti from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort -from primaite.session.io import store_nmne_config from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState @@ -27,6 +26,7 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application @@ -265,7 +265,7 @@ class PrimaiteGame: nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) # Set the NMNE capture config - NetworkInterface.nmne_config = store_nmne_config(network_config.get("nmne_config", {})) + NetworkInterface.nmne_config = NMNEConfig(**network_config.get("nmne_config", {})) for node_cfg in nodes_cfg: n_type = node_cfg["type"] diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index c634e835..78d7cb3c 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -131,48 +131,3 @@ class PrimaiteIO: new = cls(settings=cls.Settings(**config)) return new - - -class NMNEConfig(BaseModel): - """Store all the information to perform NMNE operations.""" - - capture_nmne: bool = False - """Indicates whether Malicious Network Events (MNEs) should be captured.""" - nmne_capture_keywords: List[str] = [] - """List of keywords to identify malicious network events.""" - capture_by_direction: bool = True - """Captures should be organized by traffic direction (inbound/outbound).""" - capture_by_ip_address: bool = False - """Captures should be organized by source or destination IP address.""" - capture_by_protocol: bool = False - """Captures should be organized by network protocol (e.g., TCP, UDP).""" - capture_by_port: bool = False - """Captures should be organized by source or destination port.""" - capture_by_keyword: bool = False - """Captures should be filtered and categorised based on specific keywords.""" - - -def store_nmne_config(nmne_config: Dict) -> NMNEConfig: - """ - Store configuration for capturing Malicious Network Events (MNEs). - - This function updates settings related to NMNE capture, stored in NMNEConfig including whether - to capture NMNEs and the keywords to use for identifying NMNEs. - - The function ensures that the settings are updated only if they are provided in the - `nmne_config` dictionary, and maintains type integrity by relying on pydantic validators. - - :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys - include: - "capture_nmne" (bool) to indicate whether NMNEs should be captured; - "nmne_capture_keywords" (list of strings) to specify keywords for NMNE identification. - :rvar class with data read from config file. - """ - nmne_capture_keywords: List[str] = [] - # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect - capture_nmne = nmne_config.get("capture_nmne", False) - - # Update the NMNE capture keywords, appending new keywords if provided - nmne_capture_keywords += nmne_config.get("nmne_capture_keywords", []) - - return NMNEConfig(capture_nmne=capture_nmne, nmne_capture_keywords=nmne_capture_keywords) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index aafdbe5c..50549389 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -14,12 +14,12 @@ from pydantic import BaseModel, Field from primaite import getLogger from primaite.exceptions import NetworkError from primaite.interface.request import RequestResponse -from primaite.session.io import NMNEConfig from primaite.simulator import SIM_OUTPUT from primaite.simulator.core import RequestFormat, RequestManager, RequestPermissionValidator, RequestType, SimComponent from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.system.applications.application import Application diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py new file mode 100644 index 00000000..c9cff5de --- /dev/null +++ b/src/primaite/simulator/network/nmne.py @@ -0,0 +1,25 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import List + +from pydantic import BaseModel, ConfigDict + + +class NMNEConfig(BaseModel): + """Store all the information to perform NMNE operations.""" + + model_config = ConfigDict(extra="forbid") + + capture_nmne: bool = False + """Indicates whether Malicious Network Events (MNEs) should be captured.""" + nmne_capture_keywords: List[str] = [] + """List of keywords to identify malicious network events.""" + capture_by_direction: bool = True + """Captures should be organized by traffic direction (inbound/outbound).""" + capture_by_ip_address: bool = False + """Captures should be organized by source or destination IP address.""" + capture_by_protocol: bool = False + """Captures should be organized by network protocol (e.g., TCP, UDP).""" + capture_by_port: bool = False + """Captures should be organized by source or destination port.""" + capture_by_keyword: bool = False + """Captures should be filtered and categorised based on specific keywords.""" diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 7f86d26d..ef789ba7 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -9,11 +9,11 @@ from gymnasium import spaces from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.game import PrimaiteGame -from primaite.session.io import store_nmne_config from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser @@ -87,7 +87,7 @@ def test_nic(simulation): } # Apply the NMNE configuration settings - NetworkInterface.nmne_config = store_nmne_config(nmne_config) + NetworkInterface.nmne_config = NMNEConfig(**nmne_config) assert nic_obs.space["nic_status"] == spaces.Discrete(3) assert nic_obs.space["NMNE"]["inbound"] == spaces.Discrete(4) diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index b4162e58..debf5b1c 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,9 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation -from primaite.session.io import store_nmne_config from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection @@ -35,7 +35,7 @@ def test_capture_nmne(uc2_network: Network): } # Apply the NMNE configuration settings - NIC.nmne_config = store_nmne_config(nmne_config) + NIC.nmne_config = NMNEConfig(**nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers assert web_server_nic.nmne == {} @@ -112,7 +112,7 @@ def test_describe_state_nmne(uc2_network: Network): } # Apply the NMNE configuration settings - NIC.nmne_config = store_nmne_config(nmne_config) + NIC.nmne_config = NMNEConfig(**nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers web_server_nic_state = web_server_nic.describe_state() @@ -221,7 +221,7 @@ def test_capture_nmne_observations(uc2_network: Network): } # Apply the NMNE configuration settings - NIC.nmne_config = store_nmne_config(nmne_config) + NIC.nmne_config = NMNEConfig(**nmne_config) # Define observations for the NICs of the database and web servers db_server_nic_obs = NICObservation(where=["network", "nodes", "database_server", "NICs", 1], include_nmne=True) From bd1e23db7df686e1e50a5e5850a0a45c4dc509d5 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 31 Jul 2024 15:25:02 +0100 Subject: [PATCH 15/16] 2676 - make ntwk intf use default nmne config --- src/primaite/simulator/network/hardware/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 50549389..6a25cbef 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -99,7 +99,7 @@ class NetworkInterface(SimComponent, ABC): pcap: Optional[PacketCapture] = None "A PacketCapture instance for capturing and analysing packets passing through this interface." - nmne_config: ClassVar[NMNEConfig] = None + nmne_config: ClassVar[NMNEConfig] = NMNEConfig() "A dataclass defining malicious network events to be captured." nmne: Dict = Field(default_factory=lambda: {}) @@ -1167,7 +1167,7 @@ class Node(SimComponent): ip_address, network_interface.speed, "Enabled" if network_interface.enabled else "Disabled", - network_interface.nmne if self.nmne_config.capture_nmne else "Disabled", + network_interface.nmne if network_interface.nmne_config.capture_nmne else "Disabled", ] ) print(table) From b5992574339c2d28b5ab954d566d478317c4fc4e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 1 Aug 2024 09:06:35 +0100 Subject: [PATCH 16/16] #2676 - update configs to use new nmne schema; fix test and warnings --- .../_package_data/scenario_with_placeholders/scenario.yaml | 4 ++++ src/primaite/simulator/network/protocols/icmp.py | 4 ++-- tests/conftest.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index 81848b2d..dfd200f3 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -129,6 +129,10 @@ agents: simulation: network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE nodes: - hostname: client type: computer diff --git a/src/primaite/simulator/network/protocols/icmp.py b/src/primaite/simulator/network/protocols/icmp.py index 743e2375..9f0626f0 100644 --- a/src/primaite/simulator/network/protocols/icmp.py +++ b/src/primaite/simulator/network/protocols/icmp.py @@ -4,7 +4,7 @@ from enum import Enum from typing import Union from pydantic import BaseModel, field_validator, validate_call -from pydantic_core.core_schema import FieldValidationInfo +from pydantic_core.core_schema import ValidationInfo from primaite import getLogger @@ -96,7 +96,7 @@ class ICMPPacket(BaseModel): @field_validator("icmp_code") # noqa @classmethod - def _icmp_type_must_have_icmp_code(cls, v: int, info: FieldValidationInfo) -> int: + def _icmp_type_must_have_icmp_code(cls, v: int, info: ValidationInfo) -> int: """Validates the icmp_type and icmp_code.""" icmp_type = info.data["icmp_type"] if get_icmp_type_code_description(icmp_type, v): diff --git a/tests/conftest.py b/tests/conftest.py index 54519e2b..2996e953 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,7 @@ from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer from tests import TEST_ASSETS_ROOT -rayinit(local_mode=True) +rayinit() ACTION_SPACE_NODE_VALUES = 1 ACTION_SPACE_NODE_ACTION_VALUES = 1