From bc37d8614a85066f05b79611f2335a69b3927e52 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 3 Nov 2022 09:07:55 +0000 Subject: [PATCH 1/7] AIDT-67: initial config data class structure --- yawning_titan/config/agents/__init__.py | 0 .../config/agents/blue_agent_config.py | 322 ++++++++++++++++++ .../config/agents/red_agent_config.py | 312 +++++++++++++++++ yawning_titan/config/environment/__init__.py | 0 .../config/environment/game_rules_config.py | 20 ++ .../environment/observation_space_config.py | 87 +++++ .../config/environment/reset_config.py | 53 +++ .../config/environment/rewards_config.py | 75 ++++ yawning_titan/config/game_config/__init__.py | 0 .../config/game_config/config_group_class.py | 23 ++ .../config/game_config/game_mode_config.py | 58 ++++ .../game_config/game_mode_config_builder.py | 49 +++ 12 files changed, 999 insertions(+) create mode 100644 yawning_titan/config/agents/__init__.py create mode 100644 yawning_titan/config/agents/blue_agent_config.py create mode 100644 yawning_titan/config/agents/red_agent_config.py create mode 100644 yawning_titan/config/environment/__init__.py create mode 100644 yawning_titan/config/environment/game_rules_config.py create mode 100644 yawning_titan/config/environment/observation_space_config.py create mode 100644 yawning_titan/config/environment/reset_config.py create mode 100644 yawning_titan/config/environment/rewards_config.py create mode 100644 yawning_titan/config/game_config/__init__.py create mode 100644 yawning_titan/config/game_config/config_group_class.py create mode 100644 yawning_titan/config/game_config/game_mode_config.py create mode 100644 yawning_titan/config/game_config/game_mode_config_builder.py diff --git a/yawning_titan/config/agents/__init__.py b/yawning_titan/config/agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yawning_titan/config/agents/blue_agent_config.py b/yawning_titan/config/agents/blue_agent_config.py new file mode 100644 index 00000000..2bc82e33 --- /dev/null +++ b/yawning_titan/config/agents/blue_agent_config.py @@ -0,0 +1,322 @@ +from dataclasses import dataclass +from typing import Any, Dict + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC +from yawning_titan.envs.generic.helpers.environment_input_validation import check_within_range, check_type + + +@dataclass() +class BlueAgentConfig(ConfigGroupABC): + """ + Class that validates and stores the Blue Agent Configuration + """ + + blue_max_deceptive_nodes: int + """Integer value specifying how many deceptive nodes the blue agent can place in the network""" + + blue_immediate_detection_chance: float + """Chance for the blue agent to immediately discover the node compromised by the red agent""" + + blue_scan_detection_chance: float + """Chance for the blue agent to discover a compromised node on scan""" + + blue_deception_immediate_detection_chance: float + """Chance for the blue agent to immediately discover that a deceptive node has been compromised""" + + blue_deception_scan_detection_chance: float + """Chance for the blue agent to discover that a deceptive node has been compromised on scan""" + + blue_discover_failed_attacks: bool + """Is true if blue agent can discover failed attacks""" + + blue_discover_attack_source_if_detected: bool + """Is true if the blue agent can learn information about an attack that succeeds if the compromise is known""" + + blue_discover_attack_source_if_not_detected: bool + """Is true if the blue agent can learn information about an attack that succeeds if the compromise is not known""" + + blue_chance_to_discover_source_failed: float + """Chance for blue to discover information about a failed attack""" + + blue_chance_to_discover_source_succeed_known: float + """Chance for blue to discover information about an attack that succeeded and the compromise was known""" + + blue_chance_to_discover_source_succeed_unknown: float + """Chance for blue to discover information about an attack that succeeded and the compromise was not known""" + + blue_chance_to_discover_source_deceptive_failed: float + """Chance to discover the location of a failed attack on a deceptive node""" + + blue_chance_to_discover_source_deceptive_succeed: float + """Chance to discover the location of a succeeded attack against a deceptive node""" + + blue_make_node_safe_modifies_vuln: bool + """Is true if blue agent can fix a node to decrease its vulnerability""" + + blue_vuln_change_amount_make_safe: float + """The amount the vulnerability score will change when blue agent fixes a node""" + + blue_make_safe_random_vuln: bool + """Is true if the vulnerability score is randomised when blue agent fixes a node""" + + blue_reduce_vuln_action: bool + """Is true if blue agent will try to reduce a node's vulnerability score""" + + blue_restore_node_action: bool + """Is true if the blue agent will try to reset the node state to what it was at the beginning of the game""" + + blue_make_node_safe_action: bool + """Is true if the blue agent will try to fix a node without resetting the node to what it was at the beginning + of the game""" + + blue_scan_action: bool + """Is true if blue can scan all nodes in the network to detect red agent intrusions""" + + blue_isolate_action: bool + """Is true if blue agent can isolate a node i.e. remove connections to and from a node""" + + blue_reconnect_action: bool + """Is true if blue agent can reinstate node connections after the node is isolated""" + + blue_do_nothing_action: bool + """Is true if the blue agent can decide to do nothing""" + + blue_deceptive_action: bool + """Is true if the blue agent can use deceptive nodes""" + + blue_deceptive_node_make_new: bool + """Is true if the blue agent can reuse a deceptive node if it has run out of deceptive nodes it can place""" + + @classmethod + def create( + cls, + settings: Dict[str, Any], + ): + # validate blue agent config values + cls._validate(settings) + + blue_agent = BlueAgentConfig( + blue_max_deceptive_nodes=settings["max_number_deceptive_nodes"], + blue_immediate_detection_chance=settings[ + "chance_to_immediately_discover_intrusion" + ], + blue_scan_detection_chance=settings[ + "chance_to_discover_intrusion_on_scan" + ], + blue_deception_immediate_detection_chance=settings[ + "chance_to_immediately_discover_intrusion_deceptive_node" + ], + blue_deception_scan_detection_chance=settings[ + "chance_to_discover_intrusion_on_scan_deceptive_node" + ], + blue_discover_failed_attacks=settings[ + "can_discover_failed_attacks" + ], + blue_discover_attack_source_if_detected=settings[ + "can_discover_succeeded_attacks_if_compromise_is_discovered" + ], + blue_discover_attack_source_if_not_detected=settings[ + "can_discover_succeeded_attacks_if_compromise_is_not_discovered" + ], + blue_chance_to_discover_source_failed=settings[ + "chance_to_discover_failed_attack" + ], + blue_chance_to_discover_source_succeed_known=settings[ + "chance_to_discover_succeeded_attack_compromise_known" + ], + blue_chance_to_discover_source_succeed_unknown=settings[ + "chance_to_discover_succeeded_attack_compromise_not_known" + ], + blue_chance_to_discover_source_deceptive_failed=settings[ + "chance_to_discover_failed_attack_deceptive_node" + ], + blue_chance_to_discover_source_deceptive_succeed=settings[ + "chance_to_discover_succeeded_attack_deceptive_node" + ], + blue_make_node_safe_modifies_vuln=settings[ + "making_node_safe_modifies_vulnerability" + ], + blue_vuln_change_amount_make_safe=settings[ + "vulnerability_change_during_node_patch" + ], + blue_make_safe_random_vuln=settings[ + "making_node_safe_gives_random_vulnerability" + ], + blue_reduce_vuln_action=settings[ + "blue_uses_reduce_vulnerability" + ], + blue_restore_node_action=settings["blue_uses_restore_node"], + blue_make_node_safe_action=settings["blue_uses_make_node_safe"], + blue_scan_action=settings["blue_uses_scan"], + blue_isolate_action=settings["blue_uses_isolate_node"], + blue_reconnect_action=settings["blue_uses_reconnect_node"], + blue_do_nothing_action=settings["blue_uses_do_nothing"], + blue_deceptive_action=settings["blue_uses_deceptive_nodes"], + blue_deceptive_node_make_new=settings[ + "relocating_deceptive_nodes_generates_a_new_node" + ] + ) + + return blue_agent + + @classmethod + def _validate(cls, data: dict): + # data is int or float + for name in [ + "chance_to_immediately_discover_intrusion", + "chance_to_discover_intrusion_on_scan", + "vulnerability_change_during_node_patch", + "chance_to_discover_failed_attack", + "chance_to_discover_succeeded_attack_compromise_known", + "chance_to_discover_succeeded_attack_compromise_not_known", + "chance_to_immediately_discover_intrusion_deceptive_node", + "chance_to_discover_intrusion_on_scan_deceptive_node", + "chance_to_discover_failed_attack_deceptive_node", + "chance_to_discover_succeeded_attack_deceptive_node", + "vulnerability_change_during_node_patch", + ]: + check_type(data, name, [int, float]) + # data is int + for name in ["max_number_deceptive_nodes"]: + check_type(data, name, [int]) + + # data is bool + for name in [ + "making_node_safe_modifies_vulnerability", + "making_node_safe_gives_random_vulnerability", + "blue_uses_reduce_vulnerability", + "blue_uses_restore_node", + "blue_uses_make_node_safe", + "blue_uses_scan", + "blue_uses_isolate_node", + "blue_uses_reconnect_node", + "blue_uses_do_nothing", + "blue_uses_deceptive_nodes", + "can_discover_failed_attacks", + "can_discover_succeeded_attacks_if_compromise_is_discovered", + "can_discover_succeeded_attacks_if_compromise_is_not_discovered", + "relocating_deceptive_nodes_generates_a_new_node", + ]: + check_type(data, name, [bool]) + + # data is between 0 and 1 inclusive + for name in [ + "chance_to_immediately_discover_intrusion", + "chance_to_immediately_discover_intrusion_deceptive_node", + "chance_to_discover_intrusion_on_scan_deceptive_node", + "chance_to_discover_intrusion_on_scan", + "chance_to_discover_failed_attack", + "chance_to_discover_succeeded_attack_compromise_known", + "chance_to_discover_succeeded_attack_compromise_not_known", + "chance_to_discover_failed_attack_deceptive_node", + "chance_to_discover_succeeded_attack_deceptive_node", + ]: + check_within_range(data, name, 0, 1, True, True) + + check_within_range( + data, "vulnerability_change_during_node_patch", -1, 1, True, True + ) + + # misc + if ( + not data["blue_uses_reduce_vulnerability"] + and (not data["blue_uses_restore_node"]) + and (not data["blue_uses_make_node_safe"]) + and (not data["blue_uses_scan"]) + and (not data["blue_uses_isolate_node"]) + and (not data["blue_uses_reconnect_node"]) + and (not data["blue_uses_do_nothing"]) + and (not data["blue_uses_deceptive_nodes"]) + ): + raise ValueError( + "'blue_uses_****' -> Blue must have at least one action selected. If you want blue to do nothing set 'blue_uses_do_nothing' to True" + # noqa + ) + + if (data["blue_uses_isolate_node"] and (not data["blue_uses_reconnect_node"])) or ( + (not data["blue_uses_isolate_node"]) and data["blue_uses_reconnect_node"] + ): + raise ValueError( + "'blue_uses_isolate_node', 'blue_uses_reconnect_node' -> Blue should be able to reconnect or isolate nodes if the other is true" + # noqa + ) + + check_within_range(data, "max_number_deceptive_nodes", 0, None, True, True) + + if data["blue_uses_deceptive_nodes"] and (0 == data["max_number_deceptive_nodes"]): + raise ValueError( + "'blue_uses_deceptive_nodes', 'max_number_deceptive_nodes' -> If blue can use deceptive nodes then max_number_deceptive_nodes." + # noqa + ) + + if data["blue_uses_scan"]: + if data["chance_to_immediately_discover_intrusion"] == 1: + raise ValueError( + "'blue_uses_scan', 'chance_to_immediately_discover_intrusion' -> The scan action is selected yet blue has 100% chance to spot detections. There is no need for the blue to have the scan action in this case" + # noqa + ) + else: + if data["chance_to_immediately_discover_intrusion"] != 1: + raise ValueError( + "'blue_uses_scan', 'chance_to_immediately_discover_intrusion' -> If the blue agent cannot scan nodes then it should be able to automtically detect the intrusions" + # noqa + ) + + if ( + data["chance_to_discover_intrusion_on_scan_deceptive_node"] + <= data["chance_to_discover_intrusion_on_scan"] + ): + if data["chance_to_discover_intrusion_on_scan_deceptive_node"] != 1: + raise ValueError( + "'chance_to_discover_intrusion_on_scan_deceptive_node', 'chance_to_discover_intrusion_on_scan' -> The deceptive nodes should have a higher chance at detecting intrusions that the regular nodes" + # noqa + ) + + if ( + data["chance_to_discover_failed_attack_deceptive_node"] + <= data["chance_to_discover_failed_attack"] + ): + if data["chance_to_discover_failed_attack_deceptive_node"] != 1: + raise ValueError( + "'chance_to_discover_failed_attack_deceptive_node', 'chance_to_discover_failed_attack' -> The deceptive nodes should have a higher chance at detecting intrusions that the regular nodes" + # noqa + ) + + if ( + data["chance_to_discover_succeeded_attack_deceptive_node"] + <= data["chance_to_discover_succeeded_attack_compromise_known"] + ): + if data["chance_to_discover_succeeded_attack_deceptive_node"] != 1: + raise ValueError( + "'chance_to_discover_succeeded_attack_deceptive_node', 'chance_to_discover_succeeded_attack_compromise_known' -> The deceptive nodes should have a higher chance at detecting intrusions that the regular nodes" + # noqa + ) + + if ( + data["chance_to_discover_succeeded_attack_deceptive_node"] + <= data["chance_to_discover_succeeded_attack_compromise_not_known"] + ): + if data["chance_to_discover_succeeded_attack_deceptive_node"] != 1: + raise ValueError( + "'chance_to_discover_succeeded_attack_deceptive_node', 'chance_to_discover_succeeded_attack_compromise_not_known' -> The deceptive nodes should have a higher chance at detecting intrusions that the regular nodes" + # noqa + ) + + if ( + data["making_node_safe_gives_random_vulnerability"] + and data["making_node_safe_modifies_vulnerability"] + ): + raise ValueError( + "'making_node_safe_gives_random_vulnerability', 'making_node_safe_modifies_vulnerability' -> Does not make sense to give a node a random vulnerability and to increase its vulnerability when a node is made safe" + # noqa + ) + + if ( + data["chance_to_immediately_discover_intrusion_deceptive_node"] + <= data["chance_to_immediately_discover_intrusion"] + ): + if data["chance_to_immediately_discover_intrusion_deceptive_node"] != 1: + raise ValueError( + "'chance_to_immediately_discover_intrusion_deceptive_node', 'chance_to_immediately_discover_intrusion' -> The deceptive nodes should have a higher chance at detecting intrusions that the regular nodes" + # noqa + ) diff --git a/yawning_titan/config/agents/red_agent_config.py b/yawning_titan/config/agents/red_agent_config.py new file mode 100644 index 00000000..d2f0748c --- /dev/null +++ b/yawning_titan/config/agents/red_agent_config.py @@ -0,0 +1,312 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, Any + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC +from yawning_titan.envs.generic.helpers.environment_input_validation import check_type, check_within_range + + +@dataclass() +class RedAgentConfig(ConfigGroupABC): + """ + Class that validates and stores the Red Agent Configuration + """ + + # red agent skill + red_skill: float + """Red agent's skill modifier""" + + red_use_skill: bool + """Is true if red agent will use the skill modifier when attacking a node""" + + # red agent attack pattern + red_ignore_defences: bool + """Is true if red agent will ignore node defences""" + + red_always_succeeds: bool + """Is true if red agent will always succeed when attacking""" + + red_attack_from_current_position: bool + """Is true if red agent can only attack from its current position""" + + red_attack_from_any_node: bool + """Is true if red can attack any safe node anywhere in the network""" + + # red spread + red_naturally_spread: bool + """Is true if red can naturally spread every timestep""" + + red_chance_to_spread_to_connected_node: float + """Chance of red agent spreading to a connected safe node""" + + red_chance_to_spread_to_unconnected_node: float + """Chance of red agent spreading to an unconnected safe node""" + + red_spread_action: bool + """Is true if red can try to spread to every connected safe node""" + + red_spread_action_likelihood: float + """Chance of red agent to try and spread to every connected safe node""" + + red_spread_success_chance: float + """Chance for red agent spread action to succeed""" + + red_random_infection_action: bool + """Is true if red agent can attack any safe node in the network""" + + red_random_infection_likelihood: float + """Chance of the red agent attacking any random safe node""" + + red_random_infection_success_chance: float + """Chance of the random safe node attacks from succeeding""" + + red_basic_attack_action: bool + """Is true if red uses a basic attack to take over a safe node connected to an infected node""" + + red_basic_attack_likelihood: float + """Chance of the basic attack succeeding""" + + # red do nothing + red_do_nothing_action: bool + """Is true if the red agent can choose to do nothing""" + red_do_nothing_likelihood: float + """Chance of the red agent from doing nothing""" + + # red movement + red_move_action: bool + """Is true if the red agent can choose to move to another infected node""" + + red_move_action_likelihood: float + """Chance of red agent choosing to move to another infected node""" + + # red zero days + red_zero_day_action: bool + """Is true if the red agent can use a zero day to infect a node with 100% success""" + + red_zero_day_start_amount: int + """Integer value specifying how many zero days the red agent can use at the start of the game""" + + red_zero_day_days_required_to_create: int + """Integer value specifying how many timesteps is needed until the red agent can get another zero day""" + + # red targeting + red_targeting_random: bool + """Is true if the red agent targets safe nodes at random""" + + red_targeting_prioritise_connected_nodes: bool + """Is true if the red agent prioritises attacking nodes with the most connections""" + + red_targeting_prioritise_unconnected_nodes: bool + """Is true if the red agent prioritises attacking nodes with the least connections""" + + red_targeting_prioritise_vulnerable_nodes: bool + """Is true if the red agent prioritises attacking nodes with the most vulnerability""" + + red_targeting_prioritise_resilient_nodes: bool + """Is true if the red agent prioritises attacking nodes with the least vulnerability""" + + @classmethod + def create( + cls, + settings: Dict[str, Any] + ) -> RedAgentConfig: + # validate red agent config values + cls._validate(settings) + + red_agent = RedAgentConfig( + + red_skill=settings["red_skill"], + red_use_skill=settings["red_uses_skill"], + red_ignore_defences=settings["red_ignores_defences"], + red_always_succeeds=settings["red_always_succeeds"], + red_attack_from_current_position=settings[ + "red_can_only_attack_from_red_agent_node" + ], + red_attack_from_any_node=settings[ + "red_can_attack_from_any_red_node" + ], + red_naturally_spread=settings["red_can_naturally_spread"], + red_chance_to_spread_to_connected_node=settings[ + "chance_to_spread_to_connected_node" + ], + red_chance_to_spread_to_unconnected_node=settings[ + "chance_to_spread_to_unconnected_node" + ], + red_spread_action=settings["red_uses_spread_action"], + red_spread_action_likelihood=settings[ + "spread_action_likelihood" + ], + red_spread_success_chance=settings["chance_for_red_to_spread"], + red_random_infection_action=settings[ + "red_uses_random_infect_action" + ], + red_random_infection_likelihood=settings[ + "random_infect_action_likelihood" + ], + red_random_infection_success_chance=settings[ + "chance_for_red_to_random_compromise" + ], + red_basic_attack_action=settings["red_uses_basic_attack_action"], + red_basic_attack_likelihood=settings[ + "basic_attack_action_likelihood" + ], + red_do_nothing_action=settings["red_uses_do_nothing_action"], + red_do_nothing_likelihood=settings[ + "do_nothing_action_likelihood" + ], + red_move_action=settings["red_uses_move_action"], + red_move_action_likelihood=settings["move_action_likelihood"], + red_zero_day_action=settings["red_uses_zero_day_action"], + red_zero_day_start_amount=settings["zero_day_start_amount"], + red_zero_day_days_required_to_create=settings[ + "days_required_for_zero_day" + ], + red_targeting_random=settings["red_chooses_target_at_random"], + red_targeting_prioritise_connected_nodes=settings[ + "red_prioritises_connected_nodes" + ], + red_targeting_prioritise_unconnected_nodes=settings[ + "red_prioritises_un_connected_nodes" + ], + red_targeting_prioritise_vulnerable_nodes=settings[ + "red_prioritises_vulnerable_nodes" + ], + red_targeting_prioritise_resilient_nodes=settings[ + "red_prioritises_resilient_nodes" + ] + ) + + return red_agent + + @classmethod + def _validate( + cls, + data: dict + ): + """ + Validate the red agent configuration + + Args: + data: dictionary of the red agent configuration + """ + for name in [ + "chance_for_red_to_spread", + "chance_for_red_to_random_compromise", + "red_skill", + "spread_action_likelihood", + "random_infect_action_likelihood", + "basic_attack_action_likelihood", + "do_nothing_action_likelihood", + "move_action_likelihood", + "chance_to_spread_to_connected_node", + "chance_to_spread_to_unconnected_node", + ]: + check_type(data, name, [int, float]) + + # int + for name in ["zero_day_start_amount", "days_required_for_zero_day"]: + check_type(data, name, [int]) + + # type of data is bool + for name in [ + "red_uses_skill", + "red_ignores_defences", + "red_always_succeeds", + "red_can_only_attack_from_red_agent_node", + "red_can_attack_from_any_red_node", + "red_uses_spread_action", + "red_uses_random_infect_action", + "red_uses_zero_day_action", + "red_uses_basic_attack_action", + "red_uses_do_nothing_action", + "red_uses_move_action", + "red_chooses_target_at_random", + "red_prioritises_connected_nodes", + "red_prioritises_un_connected_nodes", + "red_prioritises_vulnerable_nodes", + "red_prioritises_resilient_nodes", + "red_can_naturally_spread", + ]: + check_type(data, name, [bool]) + + # data satisfies 0 <= data <= 1 + for name in [ + "red_skill", + "chance_for_red_to_spread", + "chance_for_red_to_random_compromise", + "chance_to_spread_to_connected_node", + "chance_to_spread_to_unconnected_node", + ]: + check_within_range(data, name, 0, 1, True, True) + + # data satisfies 0 < data + for name in [ + "spread_action_likelihood", + "random_infect_action_likelihood", + "basic_attack_action_likelihood", + "do_nothing_action_likelihood", + "move_action_likelihood", + ]: + check_within_range(data, name, 0, None, False, True) + + # data satisfies 0 <= data + for name in ["zero_day_start_amount", "days_required_for_zero_day"]: + check_within_range(data, name, 0, None, True, True) + + # misc + if ( + (not data["red_uses_skill"]) + and (not data["red_always_succeeds"]) + and data["red_ignores_defences"] + ): + raise ValueError( + "'red_uses_skill', 'red_always_succeeds', 'red_ignores_defences' -> Red must either use skill, always succeed or not ignore the defences of the nodes" + # noqa + ) + if ( + (not data["red_uses_spread_action"]) + and (not data["red_uses_random_infect_action"]) + and (not data["red_uses_zero_day_action"]) + and (not data["red_uses_basic_attack_action"]) + and (not data["red_uses_do_nothing_action"]) + ): + raise ValueError( + "'red_uses_*****' -> Red must have at least one action activated" + ) + if ( + (not data["red_chooses_target_at_random"]) + and (not data["red_prioritises_connected_nodes"]) + and (not data["red_prioritises_un_connected_nodes"]) + and (not data["red_prioritises_vulnerable_nodes"]) + and (not data["red_prioritises_resilient_nodes"]) + ): + raise ValueError( + "'red_prioritises_****' -> Red must choose its target in some way. If you are unsure select 'red_chooses_target_at_random'" + # noqa + ) + if (not data["red_can_only_attack_from_red_agent_node"]) and ( + not data["red_can_attack_from_any_red_node"] + ): + raise ValueError( + "'red_can_only_attack_from_red_agent_node', 'red_can_attack_from_any_red_node' -> The red agent must be able to attack either from every red node or just the red central node" + # noqa + ) + if ( + data["red_prioritises_vulnerable_nodes"] + or data["red_prioritises_resilient_nodes"] + ): + if data["red_ignores_defences"]: + raise ValueError( + "'red_ignores_defences', 'red_prioritises_vulnerable_nodes', 'red_prioritises_resilient_nodes' -> It makes no sense for red to prioritise nodes based on a stat that is ignored (vulnerability)" + # noqa + ) + # spread both 0 but spreading on? + if data["red_can_naturally_spread"]: + if ( + data["chance_to_spread_to_connected_node"] == 0 + and data["chance_to_spread_to_unconnected_node"] == 0 + ): + raise ValueError( + "'red_can_naturally_spread', 'chance_to_spread_to_connected_node', 'chance_to_spread_to_unconnected_node' -> If red can naturally spread however the probabilities for both types of spreading are 0" + # noqa + ) diff --git a/yawning_titan/config/environment/__init__.py b/yawning_titan/config/environment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yawning_titan/config/environment/game_rules_config.py b/yawning_titan/config/environment/game_rules_config.py new file mode 100644 index 00000000..3a6ed876 --- /dev/null +++ b/yawning_titan/config/environment/game_rules_config.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, Any + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC + + +@dataclass() +class GameRulesConfig(ConfigGroupABC): + """ + Class that validates and stores Game Rules Configuration + """ + + @classmethod + def create(cls, settings: Dict[str, Any]): + pass + + @classmethod + def _validate(cls, data: dict): + pass diff --git a/yawning_titan/config/environment/observation_space_config.py b/yawning_titan/config/environment/observation_space_config.py new file mode 100644 index 00000000..b94e7314 --- /dev/null +++ b/yawning_titan/config/environment/observation_space_config.py @@ -0,0 +1,87 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, Any + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC +from yawning_titan.envs.generic.helpers.environment_input_validation import check_type + + +@dataclass() +class ObservationSpaceConfig(ConfigGroupABC): + """ + Class that validates and stores the Observation Space configuration + """ + + obs_compromised_status: bool + """Is true if the blue agent can see the compromised status of all the nodes""" + + obs_node_vuln_status: bool + """Is true if the blue agent can see the vulnerability scores of all the nodes""" + + obs_node_connections: bool + """Is true if blue agent can see what nodes are connected to what other nodes""" + + obs_avg_vuln: bool + """Is true if the blue agent can see the average vulnerability of all the nodes""" + + obs_graph_connectivity: bool + """Is true if the blue agent can see a graph connectivity score""" + + obs_attack_sources: bool + """Is true if the blue agent can see all of the nodes that have recently attacked a safe node""" + + obs_attack_targets: bool + """Is true if the blue agent can see all the nodes that have recently been attacked""" + + obs_special_nodes: bool + """Is true if the blue agent can see all of the special nodes (entry nodes, high value targets)""" + + obs_red_agent_skill: bool + """Is true if the blue agent can see the skill level of the red agent""" + + @classmethod + def create( + cls, + settings: Dict[str, Any] + ) -> ObservationSpaceConfig: + cls._validate(settings) + + observation_space = ObservationSpaceConfig( + obs_compromised_status=settings[ + "compromised_status" + ], + obs_node_vuln_status=settings["vulnerabilities"], + obs_node_connections=settings["node_connections"], + obs_avg_vuln=settings["average_vulnerability"], + obs_graph_connectivity=settings[ + "graph_connectivity" + ], + obs_attack_sources=settings["attacking_nodes"], + obs_attack_targets=settings["attacked_nodes"], + obs_special_nodes=settings["special_nodes"], + obs_red_agent_skill=settings["red_agent_skill"] + ) + + return observation_space + + @classmethod + def _validate(cls, data: dict): + all_obs = [ + "compromised_status", + "vulnerabilities", + "node_connections", + "average_vulnerability", + "graph_connectivity", + "attacking_nodes", + "attacked_nodes", + "special_nodes", + "red_agent_skill", + ] + for name in all_obs: + check_type(data, name, [bool]) + + if True not in list(map(lambda x: data[x], all_obs)): + raise ValueError( + "At least one option from OBSERVATION_SPACE must be enabled. The observation space must contain at least one item" + ) + diff --git a/yawning_titan/config/environment/reset_config.py b/yawning_titan/config/environment/reset_config.py new file mode 100644 index 00000000..0601f05d --- /dev/null +++ b/yawning_titan/config/environment/reset_config.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Any + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC +from yawning_titan.envs.generic.helpers.environment_input_validation import check_type + + +@dataclass() +class ResetConfig(ConfigGroupABC): + """ + Class that validates and stores the Reset Configuration + """ + + reset_random_vulns: bool + """Is true if the vulnerabilities are re-randomised on reset""" + + reset_move_hvt: bool + """Is true if new high value nodes are chosen on reset""" + + reset_move_entry_nodes: bool + """Is true if new entry nodes are chosen on reset""" + + @classmethod + def create( + cls, + settings: Dict[str, Any] + ) -> ResetConfig: + cls._validate(settings) + + reset_config = ResetConfig( + reset_random_vulns=settings[ + "randomise_vulnerabilities_on_reset" + ], + reset_move_hvt=settings[ + "choose_new_high_value_targets_on_reset" + ], + reset_move_entry_nodes=settings[ + "choose_new_entry_nodes_on_reset" + ] + ) + + return reset_config + + @classmethod + def _validate(cls, data: dict): + for name in [ + "randomise_vulnerabilities_on_reset", + "choose_new_high_value_targets_on_reset", + "choose_new_entry_nodes_on_reset", + ]: + check_type(data, name, [bool]) diff --git a/yawning_titan/config/environment/rewards_config.py b/yawning_titan/config/environment/rewards_config.py new file mode 100644 index 00000000..655d85ed --- /dev/null +++ b/yawning_titan/config/environment/rewards_config.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Any + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC +from yawning_titan.envs.generic.core import reward_functions +from yawning_titan.envs.generic.helpers.environment_input_validation import check_type + + +@dataclass() +class RewardsConfig(ConfigGroupABC): + """ + Class that validates and stores Rewards Configuration + """ + + reward_loss: float + """Reward for the blue agent losing""" + + reward_success: float + """Reward for the blue agent winning""" + + reward_end_multiplier: bool + """Is true if reward is multiplied by percentage of nodes not compromised""" + + reward_reduce_negative_rewards:bool + """Is true if red agent rewards are reduced the closer to the end timesteps the game ends at""" + + reward_function: str + """ + The reward method used for giving rewards: + - standard_rewards + - experimental_rewards + - one_per_timestep + - zero_reward + - safe_nodes_give_rewards + - punish_bad_actions + - num_nodes_safe + - dcbo_cost_func + """ + + @classmethod + def create( + cls, + settings: Dict[str, Any] + ) -> RewardsConfig: + rewards = RewardsConfig( + reward_loss=settings["rewards_for_loss"], + reward_success=settings["rewards_for_reaching_max_steps"], + reward_end_multiplier=settings[ + "end_rewards_are_multiplied_by_end_state" + ], + reward_reduce_negative_rewards=settings[ + "reduce_negative_rewards_for_closer_fails" + ], + reward_function=settings["reward_function"] + ) + + return rewards + + @classmethod + def _validate(cls, data: dict): + # validate types + check_type(data, "rewards_for_loss", [int, float]) + check_type(data, "rewards_for_reaching_max_steps", [int, float]) + check_type(data, "end_rewards_are_multiplied_by_end_state", [bool]) + check_type(data, "reduce_negative_rewards_for_closer_fails", [bool]) + + # make sure the reward type exists + if not hasattr(reward_functions, data["reward_function"]): + raise ValueError( + "The reward function '" + + data["reward_function"] + + "' does not exist inside: yawning_titan.envs.helpers.reward_functions" + ) diff --git a/yawning_titan/config/game_config/__init__.py b/yawning_titan/config/game_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yawning_titan/config/game_config/config_group_class.py b/yawning_titan/config/game_config/config_group_class.py new file mode 100644 index 00000000..d45388e6 --- /dev/null +++ b/yawning_titan/config/game_config/config_group_class.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Dict, Any + + +@dataclass() +class ConfigGroupABC(ABC): + + @classmethod + @abstractmethod + def create( + cls, + settings: Dict[str, Any] + ): + pass + + @classmethod + @abstractmethod + def _validate( + cls, + data: dict + ): + pass diff --git a/yawning_titan/config/game_config/game_mode_config.py b/yawning_titan/config/game_config/game_mode_config.py new file mode 100644 index 00000000..bd477234 --- /dev/null +++ b/yawning_titan/config/game_config/game_mode_config.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from logging import getLogger + +import yaml +from yaml import SafeLoader + +from yawning_titan.config.agents.red_agent_config import RedAgentConfig +from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig +from yawning_titan.config.environment.game_rules_config import GameRulesConfig +from yawning_titan.config.environment.observation_space_config import ObservationSpaceConfig +from yawning_titan.config.environment.reset_config import ResetConfig +from yawning_titan.config.environment.rewards_config import RewardsConfig +from yawning_titan.config.game_modes import default_game_mode_path + +_LOGGER = getLogger(__name__) + + +@dataclass() +class GameModeConfig: + """ + Class that holds the configuration for YAWNING-TITAN + """ + + red_agent_config: RedAgentConfig + """ + Red agent configuration object + """ + + blue_agent_config: BlueAgentConfig + """ + Blue agent configuration object + """ + + observation_space_config: ObservationSpaceConfig + """ + Observation space configuration object + """ + + game_rules_config: GameRulesConfig + """ + Game rules configuration object + """ + + reset_config: ResetConfig + """ + Reset configuration object + """ + + rewards_config: RewardsConfig + """ + Rewards configuration object + """ + + output_timestep_data_to_json: bool + """ + Is true if the timestep data is output to JSON + """ + diff --git a/yawning_titan/config/game_config/game_mode_config_builder.py b/yawning_titan/config/game_config/game_mode_config_builder.py new file mode 100644 index 00000000..fc058631 --- /dev/null +++ b/yawning_titan/config/game_config/game_mode_config_builder.py @@ -0,0 +1,49 @@ +from logging import getLogger + +from yawning_titan.config.game_config.game_mode_config import GameModeConfig + +import yaml +from yaml import SafeLoader + +from yawning_titan.config.agents.red_agent_config import RedAgentConfig +from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig +from yawning_titan.config.environment.game_rules_config import GameRulesConfig +from yawning_titan.config.environment.observation_space_config import ObservationSpaceConfig +from yawning_titan.config.environment.reset_config import ResetConfig +from yawning_titan.config.environment.rewards_config import RewardsConfig +from yawning_titan.config.game_modes import default_game_mode_path + +_LOGGER = getLogger(__name__) + + +class GameModeConfigBuilder: + """ + Class Builder for GameModeConfig + """ + + @classmethod + def create( + cls, + config_path=None + ) -> GameModeConfig: + # opens the fle the user has specified to be the location of the settings + if not config_path: + settings_path = default_game_mode_path() + try: + with open(settings_path) as f: + settings = yaml.load(f, Loader=SafeLoader) + except FileNotFoundError as e: + msg = f"Configuration file does not exist: {settings_path}" + print(msg) # TODO: Remove once proper logging is setup + _LOGGER.critical(msg, exc_info=True) + raise e + + return GameModeConfig( + red_agent_config=RedAgentConfig.create(settings["RED"]), + blue_agent_config=BlueAgentConfig.create(settings["BLUE"]), + observation_space_config=ObservationSpaceConfig.create(settings["OBSERVATION_SPACE"]), + game_rules_config=GameRulesConfig.create(settings["GAME_RULES"]), + reset_config=ResetConfig.create(settings["RESET"]), + rewards_config=RewardsConfig.create(settings["REWARDS"]), + output_timestep_data_to_json=True + ) From bfd4dc7ddfc1f5a7112f68af2a8f04e413e74bf9 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 3 Nov 2022 15:24:57 +0000 Subject: [PATCH 2/7] AIDT-67: game rules config class + test setup --- tests/unit_tests/config/agents/__init__.py | 0 .../config/agents/test_blue_agent_config.py | 0 .../config/agents/test_red_agent_config.py | 0 .../unit_tests/config/environment/__init__.py | 0 .../environment/test_game_rules_config.py | 0 .../config/environment/test_network_config.py | 0 .../test_observation_space_config.py | 0 .../config/environment/test_reset_config.py | 0 .../config/environment/test_rewards_config.py | 0 .../unit_tests/config/game_config/__init__.py | 0 .../game_config/test_game_mode_config.py | 0 .../test_game_mode_config_builder.py | 0 .../config/environment/game_rules_config.py | 216 +++++++++++++++++- .../config/environment/network_config.py | 66 ++++++ .../config/game_config/config_group_class.py | 4 +- .../game_config/game_mode_config_builder.py | 7 +- 16 files changed, 286 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/config/agents/__init__.py create mode 100644 tests/unit_tests/config/agents/test_blue_agent_config.py create mode 100644 tests/unit_tests/config/agents/test_red_agent_config.py create mode 100644 tests/unit_tests/config/environment/__init__.py create mode 100644 tests/unit_tests/config/environment/test_game_rules_config.py create mode 100644 tests/unit_tests/config/environment/test_network_config.py create mode 100644 tests/unit_tests/config/environment/test_observation_space_config.py create mode 100644 tests/unit_tests/config/environment/test_reset_config.py create mode 100644 tests/unit_tests/config/environment/test_rewards_config.py create mode 100644 tests/unit_tests/config/game_config/__init__.py create mode 100644 tests/unit_tests/config/game_config/test_game_mode_config.py create mode 100644 tests/unit_tests/config/game_config/test_game_mode_config_builder.py create mode 100644 yawning_titan/config/environment/network_config.py diff --git a/tests/unit_tests/config/agents/__init__.py b/tests/unit_tests/config/agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/agents/test_blue_agent_config.py b/tests/unit_tests/config/agents/test_blue_agent_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/agents/test_red_agent_config.py b/tests/unit_tests/config/agents/test_red_agent_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/environment/__init__.py b/tests/unit_tests/config/environment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/environment/test_game_rules_config.py b/tests/unit_tests/config/environment/test_game_rules_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/environment/test_network_config.py b/tests/unit_tests/config/environment/test_network_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/environment/test_observation_space_config.py b/tests/unit_tests/config/environment/test_observation_space_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/environment/test_reset_config.py b/tests/unit_tests/config/environment/test_reset_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/environment/test_rewards_config.py b/tests/unit_tests/config/environment/test_rewards_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/game_config/__init__.py b/tests/unit_tests/config/game_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/game_config/test_game_mode_config.py b/tests/unit_tests/config/game_config/test_game_mode_config.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/config/game_config/test_game_mode_config_builder.py b/tests/unit_tests/config/game_config/test_game_mode_config_builder.py new file mode 100644 index 00000000..e69de29b diff --git a/yawning_titan/config/environment/game_rules_config.py b/yawning_titan/config/environment/game_rules_config.py index 3a6ed876..94f27e90 100644 --- a/yawning_titan/config/environment/game_rules_config.py +++ b/yawning_titan/config/environment/game_rules_config.py @@ -2,7 +2,9 @@ from dataclasses import dataclass from typing import Dict, Any +from yawning_titan.config.environment.network_config import NetworkConfig from yawning_titan.config.game_config.config_group_class import ConfigGroupABC +from yawning_titan.envs.generic.helpers.environment_input_validation import check_type, check_within_range @dataclass() @@ -11,10 +13,216 @@ class GameRulesConfig(ConfigGroupABC): Class that validates and stores Game Rules Configuration """ + gr_node_vuln_lower: float + """Lower bound of the node vulnerability""" + + gr_node_vuln_upper: float + """Upper bound of the node vulnerability""" + + gr_max_steps: int + """Timesteps the game will go on for""" + + gr_loss_total_compromise: bool + """Is true if the game ends when all nodes are lost""" + + gr_loss_pc_nodes_compromised: bool + """Is true if the game ends when a percentage of nodes is compromised""" + + gr_loss_pc_node_compromised_pc: float + """Percentage of the nodes becoming infected for the game to be considered lost by the blue agent""" + + gr_number_of_high_value_targets: int + """Number of nodes to be marked as high value in network""" + + gr_loss_hvt: bool + """Is true if the game ends if the high value node is lost""" + + gr_loss_hvt_random_placement: bool + """Is true if the high value nodes are set randomly across the network""" + + gr_loss_hvt_furthest_away: bool + """Is true if the high value nodes are set furthest away from the entry nodes""" + + gr_random_entry_nodes: bool + """Is true if the entry nodes will be placed randomly across the network""" + + gr_num_entry_nodes: int + """Number of nodes to be marked as entry nodes in network""" + + gr_prefer_central_entry: bool + """Is true if the entry nodes will be placed centrally in the network""" + + gr_prefer_edge_nodes: bool + """Is true if the entry nodes will be placed on the edges of the network""" + + gr_grace_period: int + """Number of timesteps the blue agent has to prepare""" + @classmethod - def create(cls, settings: Dict[str, Any]): - pass + def create( + cls, + settings: Dict[str, Any], + network_config: NetworkConfig + ) -> GameRulesConfig: + cls._validate(settings, network_config) + + if not network_config.high_value_targets: + gr_number_of_high_value_targets = settings[ + "number_of_high_value_targets" + ] + else: + gr_number_of_high_value_targets = len(network_config.matrix) + + game_rule_config = GameRulesConfig( + gr_node_vuln_lower=settings[ + "node_vulnerability_lower_bound" + ], + gr_node_vuln_upper=settings[ + "node_vulnerability_upper_bound" + ], + gr_max_steps=settings["max_steps"], + gr_loss_total_compromise=settings[ + "lose_when_all_nodes_lost" + ], + gr_loss_pc_nodes_compromised=settings[ + "lose_when_n_percent_of_nodes_lost" + ], + gr_loss_pc_node_compromised_pc=settings[ + "percentage_of_nodes_compromised_equals_loss" + ], + gr_number_of_high_value_targets=gr_number_of_high_value_targets, + gr_loss_hvt=settings["lose_when_high_value_target_lost"], + gr_loss_hvt_random_placement=settings[ + "choose_high_value_targets_placement_at_random" + ], + gr_loss_hvt_furthest_away=settings[ + "choose_high_value_targets_furthest_away_from_entry" + ], + gr_random_entry_nodes=settings[ + "choose_entry_nodes_randomly" + ], + gr_num_entry_nodes= + settings["number_of_entry_nodes"], + gr_prefer_central_entry=settings[ + "prefer_central_nodes_for_entry_nodes" + ], + gr_prefer_edge_nodes=settings[ + "prefer_edge_nodes_for_entry_nodes" + ], + gr_grace_period=settings["grace_period_length"] + ) + + return game_rule_config @classmethod - def _validate(cls, data: dict): - pass + def _validate( + cls, + data: dict, + network_config: NetworkConfig + ): + high_value_targets = network_config.high_value_targets + number_of_nodes = len(network_config.matrix) + + # data is int or float + for name in [ + "node_vulnerability_lower_bound", + "node_vulnerability_upper_bound", + "percentage_of_nodes_compromised_equals_loss", + ]: + check_type(data, name, [float, int]) + # data s between 0 and 1 inclusive + for name in ["node_vulnerability_lower_bound", "node_vulnerability_upper_bound"]: + check_within_range(data, name, 0, 1, True, True) + + if data["node_vulnerability_lower_bound"] > data["node_vulnerability_upper_bound"]: + raise ValueError( + "'node_vulnerability_lower_bound', 'node_vulnerability_upper_bound' -> The lower bound for the node vulnerabilities should be less than the upper bound" + # noqa + ) + check_type(data, "max_steps", [int]) + check_type(data, "number_of_entry_nodes", [int]) + check_type(data, "grace_period_length", [int]) + # ignore if high value targets passed in + if not high_value_targets: + check_type(data, "number_of_high_value_targets", [int]) + check_within_range(data, "number_of_high_value_targets", 1, number_of_nodes, True, True) + else: + # make sure the passed high value targets do not exceed the number of nodes in network + check_within_range({'hvt_length': len(high_value_targets)}, "hvt_length", 1, number_of_nodes, True, + True) + + check_within_range(data, "grace_period_length", 0, 100, True, True) + check_within_range(data, "max_steps", 0, 10000000, False, True) + check_within_range(data, "number_of_entry_nodes", 0, number_of_nodes, False, True) + + # data is boolean + for name in [ + "lose_when_all_nodes_lost", + "lose_when_n_percent_of_nodes_lost", + "lose_when_high_value_target_lost", + "choose_high_value_targets_placement_at_random", + "choose_high_value_targets_furthest_away_from_entry", + "choose_entry_nodes_randomly", + "prefer_central_nodes_for_entry_nodes", + "prefer_edge_nodes_for_entry_nodes", + ]: + check_type(data, name, [bool]) + + check_within_range( + data, "percentage_of_nodes_compromised_equals_loss", 0, 1, False, False + ) + if ( + data["prefer_central_nodes_for_entry_nodes"] + and data["prefer_edge_nodes_for_entry_nodes"] + ): + raise ValueError( + "'prefer_central_nodes_for_entry_nodes', 'prefer_edge_nodes_for_entry_nodes' -> cannot prefer both central and edge nodes" + # noqa + ) + + if ( + (not data["lose_when_all_nodes_lost"]) + and (not data["lose_when_n_percent_of_nodes_lost"]) + and (not data["lose_when_high_value_target_lost"]) + ): + raise ValueError( + "'lose_when_all_nodes_lost', 'lose_when_n_percent_of_nodes_lost', 'lose_when_high_value_target_lost' -> At least one loose condition must be turned on" + # noqa + ) + + if data["lose_when_high_value_target_lost"]: + # if there is no way to set high value targets + if ( + not high_value_targets and + not data["choose_high_value_targets_placement_at_random"] and + not data["choose_high_value_targets_furthest_away_from_entry"] + ): + raise ValueError( + "'choose_high_value_targets_placement_at_random', 'choose_high_value_targets_furthest_away_from_entry' -> A method of selecting the high value target must be chosen" + # noqa + ) + # if there are conflicting configurations + if ( + data["choose_high_value_targets_placement_at_random"] + and data["choose_high_value_targets_furthest_away_from_entry"] + ): + raise ValueError( + "'choose_high_value_targets_placement_at_random', 'choose_high_value_targets_furthest_away_from_entry' -> Only one method of selecting a high value target should be selected" + # noqa + ) + # if high value targets are set and these configurations are also set + if ( + high_value_targets and + (data["choose_high_value_targets_placement_at_random"] + or data["choose_high_value_targets_furthest_away_from_entry"]) + ): + raise ValueError( + "Provided high value targets: " + str( + high_value_targets) + " 'choose_high_value_targets_placement_at_random', 'choose_high_value_targets_furthest_away_from_entry' -> Only one method of selecting a high value target should be selected" + # noqa + ) + + if data["grace_period_length"] > data["max_steps"]: + raise ValueError( + "'grace_period_length', 'max_steps' -> The grace period cannot be the entire length of the game" + ) diff --git a/yawning_titan/config/environment/network_config.py b/yawning_titan/config/environment/network_config.py new file mode 100644 index 00000000..a9631e94 --- /dev/null +++ b/yawning_titan/config/environment/network_config.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import warnings +from dataclasses import dataclass +from typing import Dict, Any, List, Optional + +import numpy as np + +from yawning_titan.config.game_config.config_group_class import ConfigGroupABC + + +@dataclass() +class NetworkConfig(ConfigGroupABC): + """ + Class that validates and stores Network Configuration + """ + + matrix: np.array + """Stores the matrix dictating how each node is connected to each other""" + + positions: Dict + """Dictionary containing the positions of the nodes in the network (when displayed as a graph)""" + + entry_nodes: Optional[List[str]] + """List of entry nodes""" + + vulnerabilities: Optional[Dict] + """Dictionary containing the vulnerabilities of the nodes""" + + high_value_targets: Optional[List[str]] + """List of high value nodes""" + + @classmethod + def create( + cls, + matrix: np.array, + positions: Dict, + entry_nodes: Optional[List[str]] = None, + vulnerabilities: Optional[Dict] = None, + high_value_targets: Optional[List[str]] = None + ): + cls._validate() + + network_config = NetworkConfig( + matrix=matrix, + positions=positions, + entry_nodes=entry_nodes, + vulnerabilities=vulnerabilities, + high_value_targets=high_value_targets + ) + + return network_config + + @classmethod + def _validate( + cls, + matrix: np.array, + positions: Dict, + entry_nodes: Optional[List[str]] = None, + vulnerabilities: Optional[Dict] = None, + high_value_targets: Optional[List[str]] = None + ): + # check that no entry nodes and high value nodes intersect + if set(entry_nodes) & set(high_value_targets): + warnings.warn( + "Provided entry nodes and high value targets intersect and may cause the training to prematurely end") diff --git a/yawning_titan/config/game_config/config_group_class.py b/yawning_titan/config/game_config/config_group_class.py index d45388e6..6281c17d 100644 --- a/yawning_titan/config/game_config/config_group_class.py +++ b/yawning_titan/config/game_config/config_group_class.py @@ -10,7 +10,7 @@ class ConfigGroupABC(ABC): @abstractmethod def create( cls, - settings: Dict[str, Any] + **kwargs ): pass @@ -18,6 +18,6 @@ def create( @abstractmethod def _validate( cls, - data: dict + **kwargs ): pass diff --git a/yawning_titan/config/game_config/game_mode_config_builder.py b/yawning_titan/config/game_config/game_mode_config_builder.py index fc058631..ff603d62 100644 --- a/yawning_titan/config/game_config/game_mode_config_builder.py +++ b/yawning_titan/config/game_config/game_mode_config_builder.py @@ -1,5 +1,6 @@ from logging import getLogger +from yawning_titan.config.environment.network_config import NetworkConfig from yawning_titan.config.game_config.game_mode_config import GameModeConfig import yaml @@ -24,8 +25,12 @@ class GameModeConfigBuilder: @classmethod def create( cls, + network_config: NetworkConfig, config_path=None ) -> GameModeConfig: + """ + Creates an instance of the GameModeConfig class + """ # opens the fle the user has specified to be the location of the settings if not config_path: settings_path = default_game_mode_path() @@ -42,7 +47,7 @@ def create( red_agent_config=RedAgentConfig.create(settings["RED"]), blue_agent_config=BlueAgentConfig.create(settings["BLUE"]), observation_space_config=ObservationSpaceConfig.create(settings["OBSERVATION_SPACE"]), - game_rules_config=GameRulesConfig.create(settings["GAME_RULES"]), + game_rules_config=GameRulesConfig.create(settings=settings["GAME_RULES"], network_config=network_config), reset_config=ResetConfig.create(settings["RESET"]), rewards_config=RewardsConfig.create(settings["REWARDS"]), output_timestep_data_to_json=True From b690e53815b0a8773bd7704532a2ab7f7e18e60c Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 4 Nov 2022 15:38:02 +0000 Subject: [PATCH 3/7] AIDT-67: remove GameModeConfig dependency on NetworkConfig + test setup --- .../complete_blue_agent_config.yaml | 90 ++++++ .../complete_game_rules_config.yaml | 31 ++ .../complete_observation_config.yaml | 18 ++ .../complete_red_agent_config.yaml | 64 ++++ .../complete_reset_config.yaml | 3 + .../complete_rewards_config.yaml | 13 + tests/unit_tests/config/agents/__init__.py | 13 + .../config/agents/test_blue_agent_config.py | 285 ++++++++++++++++++ .../config/agents/test_red_agent_config.py | 241 +++++++++++++++ tests/unit_tests/config/config_test_utils.py | 10 + .../unit_tests/config/environment/__init__.py | 22 ++ .../game_modes/default_game_mode.yaml | 2 + ...th_random_infection_perfect_detection.yaml | 2 + .../multiple_high_value_targets.yaml | 2 + .../config/agents/blue_agent_config.py | 1 + .../config/environment/game_rules_config.py | 40 +-- .../config/game_config/game_mode_config.py | 30 ++ .../game_config/game_mode_config_builder.py | 54 ---- .../envs/generic/core/network_interface.py | 2 + 19 files changed, 843 insertions(+), 80 deletions(-) create mode 100644 tests/test_configs/config_sections/complete_blue_agent_config.yaml create mode 100644 tests/test_configs/config_sections/complete_game_rules_config.yaml create mode 100644 tests/test_configs/config_sections/complete_observation_config.yaml create mode 100644 tests/test_configs/config_sections/complete_red_agent_config.yaml create mode 100644 tests/test_configs/config_sections/complete_reset_config.yaml create mode 100644 tests/test_configs/config_sections/complete_rewards_config.yaml create mode 100644 tests/unit_tests/config/config_test_utils.py delete mode 100644 yawning_titan/config/game_config/game_mode_config_builder.py diff --git a/tests/test_configs/config_sections/complete_blue_agent_config.yaml b/tests/test_configs/config_sections/complete_blue_agent_config.yaml new file mode 100644 index 00000000..81925944 --- /dev/null +++ b/tests/test_configs/config_sections/complete_blue_agent_config.yaml @@ -0,0 +1,90 @@ +# The max number of deceptive nodes that blue can place +max_number_deceptive_nodes: 2 +# Can discover the location an attack came from if the attack failed +can_discover_failed_attacks: True + + +# The blue agent does not have to have perfect detection. In these settings you can change how much information blue +# can gain from the red agents actions. There are two different pieces of information blue can get: intrusions and +# attacks. + +# --Intrusions-- +# An intrusion is when the red agent takes over a node and compromises it. You can change the chance that blue has to +# be able to detect this using the "chance_to_immediately_discover_intrusion". If blue does not detect an intrusion +# then it can use the scan action to try and discover these intrusions with "chance_to_discover_intrusion_on_scan". + +# There are also deceptive nodes that blue can place down. These nodes are used as detectors to inform blue when they +# are compromised. They should have a chance to detect of 1 so that they can detect everything (at the very least +# they should have a chance to detect higher than the normal chance to detect) but you can modify it if you so wish +# with "chance_to_immediately_discover_intrusion_deceptive_node" and "chance_to_discover_intrusion_on_scan_deceptive_node" + +# --Attacks-- +# Attacks are the actual attacks that the red agent does to compromise the nodes. For example you may be able to see +# that node 14 is compromised but using the attack detection, the blue agent may be able to see that it was node 12 +# that attacked node 14. You can modify the chance for blue to see attacks that failed, succeeded (and blue was able +# to detect that the node was compromised) and attacks that succeeded and the blue agent did not detect the intrusion. + +# Again there are settings to change the likelihood that a deceptive node can detect an attack. While this should +# remain at 1, it is open for you to change. + +# --INTRUSIONS-- +# -Standard Nodes- +# Chance for blue to discover a node that red has compromised the instant red compromises the node +chance_to_immediately_discover_intrusion: 0.5 +# When blue performs the scan action this is the chance that a red intrusion is discovered +chance_to_discover_intrusion_on_scan: 1 + +# -Deceptive Nodes- +# Chance for blue to discover a deceptive node that red has compromised the instant red compromises the node +chance_to_immediately_discover_intrusion_deceptive_node: 1 +# When blue uses the scan action what is the chance that blue will detect an intrusion in a deceptive node +chance_to_discover_intrusion_on_scan_deceptive_node: 1 + +# --ATTACKS-- +# -Standard Nodes- +# Chance for blue to discover information about a failed attack +chance_to_discover_failed_attack: 1 +# Can blue learn information about an attack that succeeds if the compromise is known +can_discover_succeeded_attacks_if_compromise_is_discovered: True +# Can blue learn information about an attack that succeeds if the compromise is NOT known +can_discover_succeeded_attacks_if_compromise_is_not_discovered: True +# Chance for blue to discover information about an attack that succeeded and the compromise was known +chance_to_discover_succeeded_attack_compromise_known: 1 +# Chance for blue to discover information about an attack that succeeded and the compromise was NOT known +chance_to_discover_succeeded_attack_compromise_not_known: 1 + +# -Deceptive Nodes- +# Chance to discover the location of a failed attack on a deceptive node +chance_to_discover_failed_attack_deceptive_node: 1 +# Chance to discover the location of a succeeded attack against a deceptive node +chance_to_discover_succeeded_attack_deceptive_node: 1 + + +# If blue fixes a node then the vulnerability score of that node increases +making_node_safe_modifies_vulnerability: False +# The amount that the vulnerability of a node changes when it is made safe +vulnerability_change_during_node_patch: 0.4 +# When fixing a node the vulnerability score is randomised +making_node_safe_gives_random_vulnerability: False + +# CHOOSE AT LEAST ONE OF THE FOLLOWING 8 ITEMS +# Blue picks a node and reduces the vulnerability score +blue_uses_reduce_vulnerability: False +# Blue picks a node and restores everything about the node to its state at the beginning of the game +blue_uses_restore_node: True +# Blue fixes a node but does not restore it to its initial state +blue_uses_make_node_safe: True +# Blue scans all of the nodes to try and detect any red intrusions +blue_uses_scan: True +# Blue disables all of the connections to and from a node +blue_uses_isolate_node: False +# Blue re-connects all of the connections to and from a node +blue_uses_reconnect_node: False +# Blue agent does nothing +blue_uses_do_nothing: False +# Blue agent can place down deceptive nodes. These nodes act as just another node in the network but have a different +# chance of spotting attacks and always show when they are compromised +blue_uses_deceptive_nodes: False +# When the blue agent places a deceptive node and it has none left in stock it will "pick up" the first deceptive node that it used and "relocate it" +# When relocating a node will the stats for the node (such as the vulnerability and compromised status) be re-generated as if adding a new node or will they carry over from the "old" node +relocating_deceptive_nodes_generates_a_new_node: True diff --git a/tests/test_configs/config_sections/complete_game_rules_config.yaml b/tests/test_configs/config_sections/complete_game_rules_config.yaml new file mode 100644 index 00000000..e9d5947f --- /dev/null +++ b/tests/test_configs/config_sections/complete_game_rules_config.yaml @@ -0,0 +1,31 @@ +# Minimum number of nodes the network this game mode is allowed to run on +min_number_of_network_nodes: 18 +# A lower vulnerability means that a node is less likely to be compromised +node_vulnerability_lower_bound: 0.2 +# A higher vulnerability means that a node is more vulnerable +node_vulnerability_upper_bound: 0.8 +# The max steps that a game can go on for. If the blue agent reaches this they win +max_steps: 1000 +# The blue agent loses if all the nodes become compromised +lose_when_all_nodes_lost: False +# The blue agent loses if n% of the nodes become compromised +lose_when_n_percent_of_nodes_lost: False +# The percentage of nodes that need to be lost for blue to lose +percentage_of_nodes_compromised_equals_loss: 0.8 +# Blue loses if a special 'high value' target is lost (a node picked in the environment) +lose_when_high_value_target_lost: True +# If no high value targets are supplied, how many should be chosen +number_of_high_value_targets: 1 +# The high value target is picked at random +choose_high_value_targets_placement_at_random: False +# The node furthest away from the entry points to the network is picked as the target +choose_high_value_targets_furthest_away_from_entry: True +# If no entry nodes are supplied choose some at random +choose_entry_nodes_randomly: True +# If no entry nodes are supplied then how many should be chosen +number_of_entry_nodes: 3 +# If no entry nodes are supplied then what bias is applied to the nodes when choosing random entry nodes +prefer_central_nodes_for_entry_nodes: True +prefer_edge_nodes_for_entry_nodes: False +# The length of a grace period at the start of the game. During this time the red agent cannot act. This gives the blue agent a chance to "prepare" (A length of 0 means that there is no grace period) +grace_period_length: 0 \ No newline at end of file diff --git a/tests/test_configs/config_sections/complete_observation_config.yaml b/tests/test_configs/config_sections/complete_observation_config.yaml new file mode 100644 index 00000000..db80d48f --- /dev/null +++ b/tests/test_configs/config_sections/complete_observation_config.yaml @@ -0,0 +1,18 @@ +# The blue agent can see the compromised status of all the nodes +compromised_status: True +# The blue agent can see the vulnerability scores of all the nodes +vulnerabilities: True +# The blue agent can see what nodes are connected to what other nodes +node_connections: True +# The blue agent can see the average vulnerability of all the nodes +average_vulnerability: False +# The blue agent can see a graph connectivity score +graph_connectivity: True +# The blue agent can see all of the nodes that have recently attacked a safe node +attacking_nodes: True +# The blue agent can see all the nodes that have recently been attacked +attacked_nodes: True +# The blue agent can see all of the special nodes (entry nodes, high value targets) +special_nodes: True +# The blue agent can see the skill level of the red agent +red_agent_skill: True \ No newline at end of file diff --git a/tests/test_configs/config_sections/complete_red_agent_config.yaml b/tests/test_configs/config_sections/complete_red_agent_config.yaml new file mode 100644 index 00000000..cbee5fdc --- /dev/null +++ b/tests/test_configs/config_sections/complete_red_agent_config.yaml @@ -0,0 +1,64 @@ +# The red agents skill level. Higher means that red is more likely to succeed in attacks +red_skill: 0.5 + +# CHOOSE AT LEAST ONE OF THE FOLLOWING 3 ITEMS (red_ignore_defences: False counts as choosing an item) +# Red uses its skill modifier when attacking nodes +red_uses_skill: True +# The red agent ignores the defences of nodes +red_ignores_defences: False +# Reds attacks always succeed +red_always_succeeds: False + +# The red agent will only ever be in one node however it can control any amount of nodes. Can the red agent only +# attack from its one main node or can it attack from any node that it controls +red_can_only_attack_from_red_agent_node: False +red_can_attack_from_any_red_node: True + +# The red agent naturally spreads its influence every time-step +red_can_naturally_spread: True +# If a node is connected to a compromised node what chance does it have to become compromised every turn through natural spreading +chance_to_spread_to_connected_node: 0.05 +# If a node is not connected to a compromised node what chance does it have to become randomly infected through natural spreading +chance_to_spread_to_unconnected_node: 0 + +# CHOOSE AT LEAST ONE OF THE FOLLOWING 6 ITEMS (EACH ITEM HAS ASSOCIATED WEIGHTING) +# SPREAD: Tries to spread to every node connected to an infected node +red_uses_spread_action: False +# weighting for action +spread_action_likelihood: 1 +# chance for each 'spread' to succeed +chance_for_red_to_spread: 0.1 +# RANDOM INFECT: Tries to infect every safe node in the environment +red_uses_random_infect_action: False +# weighting for action +random_infect_action_likelihood: 1 +# chance for each 'infect' to succeed +chance_for_red_to_random_compromise: 0.1 +# BASIC ATTACK: The red agent picks a single node connected to an infected node and tries to attack and take over that node +red_uses_basic_attack_action: True +# weighting for action +basic_attack_action_likelihood: 2 +# DO NOTHING: The red agent does nothing +red_uses_do_nothing_action: True +do_nothing_action_likelihood: 1 +# The red agent moves to a different node +red_uses_move_action: False +move_action_likelihood: 1 +# ZERO DAY: The red agent will pick a safe node connected to an infect node and take it over with a 100% chance to succeed (can only happen every n timesteps) +red_uses_zero_day_action: True +# The number of zero day attacks that the red agent starts with +zero_day_start_amount: 1 +# The amount of 'progress' that need to have passed before the red agent gains a zero day attack +days_required_for_zero_day: 4 + +# CHOOSE ONE OF THE FOLLOWING 5 ITEMS +# Red picks nodes to attack at random +red_chooses_target_at_random: True +# Red sorts the nodes it can attack and chooses the one that has the most connections +red_prioritises_connected_nodes: False +# Red sorts the nodes it can attack and chooses the one that has the least connections +red_prioritises_un_connected_nodes: False +# Red sorts the nodes is can attack and chooses the one that is the most vulnerable +red_prioritises_vulnerable_nodes: False +# Red sorts the nodes is can attack and chooses the one that is the least vulnerable +red_prioritises_resilient_nodes: False diff --git a/tests/test_configs/config_sections/complete_reset_config.yaml b/tests/test_configs/config_sections/complete_reset_config.yaml new file mode 100644 index 00000000..f70a4281 --- /dev/null +++ b/tests/test_configs/config_sections/complete_reset_config.yaml @@ -0,0 +1,3 @@ +randomise_vulnerabilities_on_reset: False +choose_new_high_value_targets_on_reset: True +choose_new_entry_nodes_on_reset: True \ No newline at end of file diff --git a/tests/test_configs/config_sections/complete_rewards_config.yaml b/tests/test_configs/config_sections/complete_rewards_config.yaml new file mode 100644 index 00000000..f78ec740 --- /dev/null +++ b/tests/test_configs/config_sections/complete_rewards_config.yaml @@ -0,0 +1,13 @@ +# Rewards for the blue agent losing +rewards_for_loss: -100 +# Rewards for the blue agent winning by reaching the maximum number of steps +rewards_for_reaching_max_steps: 100 +# How good the end state is (what % blue controls) is multiplied by the rewards that blue receives for winning +end_rewards_are_multiplied_by_end_state: True +# The negative rewards from the red agent winning are reduced the closer to the end the blue agent gets +reduce_negative_rewards_for_closer_fails: True +# choose the reward method +# There are several built in example reward methods that you can choose from (shown below) +# You can also create your own reward method by copying one of the built in methods and calling it here +# built in reward methods: standard_rewards, one_per_timestep, safe_nodes_give_rewards, punish_bad_actions +reward_function: "standard_rewards" \ No newline at end of file diff --git a/tests/unit_tests/config/agents/__init__.py b/tests/unit_tests/config/agents/__init__.py index e69de29b..8c2298c2 100644 --- a/tests/unit_tests/config/agents/__init__.py +++ b/tests/unit_tests/config/agents/__init__.py @@ -0,0 +1,13 @@ +import os +from pathlib import Path +from typing import Final + +from tests import TEST_CONFIG_PATH + +TEST_BLUE_AGENT_CONFIG_PATH: Final[Path] = Path( + os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_blue_agent_config.yaml") +) + +TEST_RED_AGENT_CONFIG_PATH: Final[Path] = Path( + os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_red_agent_config.yaml") +) diff --git a/tests/unit_tests/config/agents/test_blue_agent_config.py b/tests/unit_tests/config/agents/test_blue_agent_config.py index e69de29b..2d4cbc35 100644 --- a/tests/unit_tests/config/agents/test_blue_agent_config.py +++ b/tests/unit_tests/config/agents/test_blue_agent_config.py @@ -0,0 +1,285 @@ +import os +from typing import Dict, Any + +import pytest + +from tests.unit_tests.config.agents import TEST_BLUE_AGENT_CONFIG_PATH +from tests.unit_tests.config.config_test_utils import read_yaml_file +from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig + + +def get_config_dict() -> Dict: + return read_yaml_file(TEST_BLUE_AGENT_CONFIG_PATH) + + +def test_read_valid_config(): + blue_agent = BlueAgentConfig.create(get_config_dict()) + + # max_number_deceptive_nodes + assert blue_agent.blue_max_deceptive_nodes == 2 + + # chance_to_immediately_discover_intrusion + assert blue_agent.blue_immediate_detection_chance + + # chance_to_discover_intrusion_on_scan + assert blue_agent.blue_scan_detection_chance == 1 + + # chance_to_immediately_discover_intrusion_deceptive_node + assert blue_agent.blue_deception_immediate_detection_chance == 1 + + # chance_to_discover_intrusion_on_scan_deceptive_node + assert blue_agent.blue_deception_scan_detection_chance == 1 + + # can_discover_failed_attacks + assert blue_agent.blue_discover_failed_attacks is True + + # can_discover_succeeded_attacks_if_compromise_is_discovered + assert blue_agent.blue_discover_attack_source_if_detected is True + + # can_discover_succeeded_attacks_if_compromise_is_not_discovered + assert blue_agent.blue_discover_attack_source_if_not_detected is True + + # chance_to_discover_failed_attack + assert blue_agent.blue_chance_to_discover_source_failed == 1 + + # chance_to_discover_succeeded_attack_compromise_known + assert blue_agent.blue_chance_to_discover_source_succeed_known == 1 + + # chance_to_discover_succeeded_attack_compromise_not_known + assert blue_agent.blue_chance_to_discover_source_succeed_unknown == 1 + + # chance_to_discover_failed_attack_deceptive_node + assert blue_agent.blue_chance_to_discover_source_deceptive_failed == 1 + + # chance_to_discover_succeeded_attack_deceptive_node + assert blue_agent.blue_chance_to_discover_source_deceptive_succeed == 1 + + # making_node_safe_modifies_vulnerability + assert blue_agent.blue_make_node_safe_modifies_vuln is False + + # vulnerability_change_during_node_patch + assert blue_agent.blue_vuln_change_amount_make_safe == 0.4 + + # making_node_safe_gives_random_vulnerability + assert blue_agent.blue_make_safe_random_vuln is False + + # blue_uses_reduce_vulnerability + assert blue_agent.blue_reduce_vuln_action is False + + # blue_uses_restore_node + assert blue_agent.blue_restore_node_action is True + + # blue_uses_make_node_safe + assert blue_agent.blue_make_node_safe_action is True + + # blue_uses_scan + assert blue_agent.blue_scan_action is True + + # blue_uses_isolate_node + assert blue_agent.blue_isolate_action is False + + # blue_uses_reconnect_node + assert blue_agent.blue_reconnect_action is False + + # blue_uses_do_nothing is False + assert blue_agent.blue_do_nothing_action is False + + # blue_uses_deceptive_nodes + assert blue_agent.blue_deceptive_action is False + + # relocating_deceptive_nodes_generates_a_new_node + assert blue_agent.blue_deceptive_node_make_new is True + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # INT/FLOAT TYPES + ("chance_to_immediately_discover_intrusion", True, + "'chance_to_immediately_discover_intrusion' needs to be of type: or "), + ("chance_to_discover_intrusion_on_scan", True, + "'chance_to_discover_intrusion_on_scan' needs to be of type: or "), + ("vulnerability_change_during_node_patch", True, + "'vulnerability_change_during_node_patch' needs to be of type: or "), + ("chance_to_discover_failed_attack", True, + "'chance_to_discover_failed_attack' needs to be of type: or "), + ("chance_to_discover_succeeded_attack_compromise_known", True, + "'chance_to_discover_succeeded_attack_compromise_known' needs to be of type: or "), + ("chance_to_discover_succeeded_attack_compromise_not_known", True, + "'chance_to_discover_succeeded_attack_compromise_not_known' needs to be of type: or "), + ("chance_to_immediately_discover_intrusion_deceptive_node", True, + "'chance_to_immediately_discover_intrusion_deceptive_node' needs to be of type: or "), + ("chance_to_discover_intrusion_on_scan_deceptive_node", True, + "'chance_to_discover_intrusion_on_scan_deceptive_node' needs to be of type: or "), + ("chance_to_discover_failed_attack_deceptive_node", True, + "'chance_to_discover_failed_attack_deceptive_node' needs to be of type: or "), + ("chance_to_discover_succeeded_attack_deceptive_node", True, + "'chance_to_discover_succeeded_attack_deceptive_node' needs to be of type: or "), + ("vulnerability_change_during_node_patch", True, + "'vulnerability_change_during_node_patch' needs to be of type: or "), + + # INT TYPE + ("max_number_deceptive_nodes", True, + "'max_number_deceptive_nodes' needs to be of type: "), + + # BOOLEANS + ("making_node_safe_modifies_vulnerability", "fail", + "'making_node_safe_modifies_vulnerability' needs to be of type: "), + ("making_node_safe_gives_random_vulnerability", "fail", + "'making_node_safe_gives_random_vulnerability' needs to be of type: "), + ("blue_uses_reduce_vulnerability", "fail", + "'blue_uses_reduce_vulnerability' needs to be of type: "), + ("blue_uses_restore_node", "fail", + "'blue_uses_restore_node' needs to be of type: "), + ("blue_uses_make_node_safe", "fail", + "'blue_uses_make_node_safe' needs to be of type: "), + ("blue_uses_scan", "fail", + "'blue_uses_scan' needs to be of type: "), + ("blue_uses_isolate_node", "fail", + "'blue_uses_isolate_node' needs to be of type: "), + ("blue_uses_reconnect_node", "fail", + "'blue_uses_reconnect_node' needs to be of type: "), + ("blue_uses_do_nothing", "fail", + "'blue_uses_do_nothing' needs to be of type: "), + ("blue_uses_deceptive_nodes", "fail", + "'blue_uses_deceptive_nodes' needs to be of type: "), + ("can_discover_failed_attacks", "fail", + "'can_discover_failed_attacks' needs to be of type: "), + ("can_discover_succeeded_attacks_if_compromise_is_discovered", "fail", + "'can_discover_succeeded_attacks_if_compromise_is_discovered' needs to be of type: "), + ("can_discover_succeeded_attacks_if_compromise_is_not_discovered", "fail", + "'can_discover_succeeded_attacks_if_compromise_is_not_discovered' needs to be of type: "), + ("relocating_deceptive_nodes_generates_a_new_node", "fail", + "'relocating_deceptive_nodes_generates_a_new_node' needs to be of type: "), + ] +) +def test_invalid_config_type(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + BlueAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # BETWEEN 0 and 1 + ("chance_to_immediately_discover_intrusion", 2, + "'chance_to_immediately_discover_intrusion' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_immediately_discover_intrusion", -1, + "'chance_to_immediately_discover_intrusion' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_immediately_discover_intrusion_deceptive_node", 2, + "'chance_to_immediately_discover_intrusion_deceptive_node' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_immediately_discover_intrusion_deceptive_node", -1, + "'chance_to_immediately_discover_intrusion_deceptive_node' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_intrusion_on_scan_deceptive_node", 2, + "'chance_to_discover_intrusion_on_scan_deceptive_node' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_intrusion_on_scan_deceptive_node", -1, + "'chance_to_discover_intrusion_on_scan_deceptive_node' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_intrusion_on_scan", 2, + "'chance_to_discover_intrusion_on_scan' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_intrusion_on_scan", -1, + "'chance_to_discover_intrusion_on_scan' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_failed_attack", 2, + "'chance_to_discover_failed_attack' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_failed_attack", -1, + "'chance_to_discover_failed_attack' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_succeeded_attack_compromise_known", 2, + "'chance_to_discover_succeeded_attack_compromise_known' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_succeeded_attack_compromise_known", -1, + "'chance_to_discover_succeeded_attack_compromise_known' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_succeeded_attack_compromise_not_known", 2, + "'chance_to_discover_succeeded_attack_compromise_not_known' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_succeeded_attack_compromise_not_known", -1, + "'chance_to_discover_succeeded_attack_compromise_not_known' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_failed_attack_deceptive_node", 2, + "'chance_to_discover_failed_attack_deceptive_node' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_failed_attack_deceptive_node", -1, + "'chance_to_discover_failed_attack_deceptive_node' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_discover_succeeded_attack_deceptive_node", 2, + "'chance_to_discover_succeeded_attack_deceptive_node' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_discover_succeeded_attack_deceptive_node", -1, + "'chance_to_discover_succeeded_attack_deceptive_node' Needs to have a value greater than: 0 (inclusive)"), + + # BETWEEN -1 and 1 + ("vulnerability_change_during_node_patch", 2, + "'vulnerability_change_during_node_patch' Needs to have a value less than: 1 (inclusive)"), + ("vulnerability_change_during_node_patch", -2, + "'vulnerability_change_during_node_patch' Needs to have a value greater than: -1 (inclusive)"), + ] +) +def test_invalid_config_range(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + BlueAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err + + +def test_blue_has_no_action_error(): + conf: Dict = get_config_dict() + + conf["blue_uses_reduce_vulnerability"] = False + conf["blue_uses_restore_node"] = False + conf["blue_uses_make_node_safe"] = False + conf["blue_uses_scan"] = False + conf["blue_uses_isolate_node"] = False + conf["blue_uses_reconnect_node"] = False + conf["blue_uses_do_nothing"] = False + conf["blue_uses_deceptive_nodes"] = False + + with pytest.raises(ValueError) as err_info: + BlueAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[ + 0] == "'blue_uses_****' -> Blue must have at least one action selected. If you want blue to do nothing set 'blue_uses_do_nothing' to True" + + +def test_reconnect_isolate_config(): + conf: Dict = get_config_dict() + + conf["blue_uses_isolate_node"] = True + conf["blue_uses_reconnect_node"] = False + + with pytest.raises(ValueError) as err_info: + BlueAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[ + 0] == "'blue_uses_isolate_node', 'blue_uses_reconnect_node' -> Blue should be able to reconnect or isolate nodes if the other is true" + + conf["blue_uses_isolate_node"] = False + conf["blue_uses_reconnect_node"] = True + + with pytest.raises(ValueError) as err_info: + BlueAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[ + 0] == "'blue_uses_isolate_node', 'blue_uses_reconnect_node' -> Blue should be able to reconnect or isolate nodes if the other is true" + + +def test_no_max_number_deceptive_nodes(): + conf: Dict = get_config_dict() + + conf["blue_uses_deceptive_nodes"] = True + conf["max_number_deceptive_nodes"] = 0 + + with pytest.raises(ValueError) as err_info: + BlueAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[ + 0] == "'blue_uses_deceptive_nodes', 'max_number_deceptive_nodes' -> If blue can use deceptive nodes then max_number_deceptive_nodes." diff --git a/tests/unit_tests/config/agents/test_red_agent_config.py b/tests/unit_tests/config/agents/test_red_agent_config.py index e69de29b..062416f3 100644 --- a/tests/unit_tests/config/agents/test_red_agent_config.py +++ b/tests/unit_tests/config/agents/test_red_agent_config.py @@ -0,0 +1,241 @@ +from typing import Dict, Any + +import pytest + +from tests.unit_tests.config.agents import TEST_RED_AGENT_CONFIG_PATH +from tests.unit_tests.config.config_test_utils import read_yaml_file +from yawning_titan.config.agents.red_agent_config import RedAgentConfig + + +def get_config_dict() -> Dict: + return read_yaml_file(TEST_RED_AGENT_CONFIG_PATH) + + +def test_read_valid_config(): + red_agent = RedAgentConfig.create(get_config_dict()) + + # red_skill + assert red_agent.red_skill == 0.5 + + # red_uses_skill + assert red_agent.red_use_skill is True + + # red_ignores_defences + assert red_agent.red_ignore_defences is False + + # red_always_succeeds + assert red_agent.red_always_succeeds is False + + # red_can_only_attack_from_red_agent_node + assert red_agent.red_attack_from_current_position is False + + # red_can_attack_from_any_red_node + assert red_agent.red_attack_from_any_node is True + + # red_can_naturally_spread + assert red_agent.red_naturally_spread is True + + # chance_to_spread_to_connected_node + assert red_agent.red_chance_to_spread_to_connected_node == 0.05 + + # chance_to_spread_to_unconnected_node + assert red_agent.red_chance_to_spread_to_unconnected_node == 0 + + # red_uses_spread_action + assert red_agent.red_spread_action is False + + # spread_action_likelihood + assert red_agent.red_spread_action_likelihood == 1 + + # chance_for_red_to_spread + assert red_agent.red_spread_success_chance == 0.1 + + # red_uses_random_infect_action + assert red_agent.red_random_infection_action is False + + # random_infect_action_likelihood + assert red_agent.red_random_infection_likelihood == 1 + + # chance_for_red_to_random_compromise + assert red_agent.red_random_infection_success_chance == 0.1 + + # red_uses_basic_attack_action + assert red_agent.red_basic_attack_action is True + + # basic_attack_action_likelihood + assert red_agent.red_basic_attack_likelihood == 2 + + # red_uses_do_nothing_action + assert red_agent.red_do_nothing_action is True + + # do_nothing_action_likelihood + assert red_agent.red_do_nothing_likelihood == 1 + + # red_uses_move_action + assert red_agent.red_move_action is False + + # move_action_likelihood + assert red_agent.red_move_action_likelihood == 1 + + # red_uses_zero_day_action + assert red_agent.red_zero_day_action is True + + # zero_day_start_amount + assert red_agent.red_zero_day_start_amount == 1 + + # days_required_for_zero_day + assert red_agent.red_zero_day_days_required_to_create == 4 + + # red_chooses_target_at_random + assert red_agent.red_targeting_random is True + + # red_prioritises_connected_nodes + assert red_agent.red_targeting_prioritise_connected_nodes is False + + # red_prioritises_un_connected_nodes + assert red_agent.red_targeting_prioritise_unconnected_nodes is False + + # red_prioritises_vulnerable_nodes + assert red_agent.red_targeting_prioritise_vulnerable_nodes is False + + # red_prioritises_resilient_nodes + assert red_agent.red_targeting_prioritise_resilient_nodes is False + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # INT/FLOAT TYPES + ("chance_for_red_to_spread", True, + "'chance_for_red_to_spread' needs to be of type: or "), + ("chance_for_red_to_random_compromise", True, + "'chance_for_red_to_random_compromise' needs to be of type: or "), + ("red_skill", True, + "'red_skill' needs to be of type: or "), + ("spread_action_likelihood", True, + "'spread_action_likelihood' needs to be of type: or "), + ("random_infect_action_likelihood", True, + "'random_infect_action_likelihood' needs to be of type: or "), + ("basic_attack_action_likelihood", True, + "'basic_attack_action_likelihood' needs to be of type: or "), + ("do_nothing_action_likelihood", True, + "'do_nothing_action_likelihood' needs to be of type: or "), + ("move_action_likelihood", True, + "'move_action_likelihood' needs to be of type: or "), + ("chance_to_spread_to_connected_node", True, + "'chance_to_spread_to_connected_node' needs to be of type: or "), + ("chance_to_spread_to_unconnected_node", True, + "'chance_to_spread_to_unconnected_node' needs to be of type: or "), + + # INT TYPES + ("zero_day_start_amount", True, + "'zero_day_start_amount' needs to be of type: "), + ("days_required_for_zero_day", True, + "'days_required_for_zero_day' needs to be of type: "), + + # BOOLEANS + ("red_uses_skill", "fail", + "'red_uses_skill' needs to be of type: "), + ("red_ignores_defences", "fail", + "'red_ignores_defences' needs to be of type: "), + ("red_always_succeeds", "fail", + "'red_always_succeeds' needs to be of type: "), + ("red_can_only_attack_from_red_agent_node", "fail", + "'red_can_only_attack_from_red_agent_node' needs to be of type: "), + ("red_can_attack_from_any_red_node", "fail", + "'red_can_attack_from_any_red_node' needs to be of type: "), + ("red_uses_spread_action", "fail", + "'red_uses_spread_action' needs to be of type: "), + ("red_uses_random_infect_action", "fail", + "'red_uses_random_infect_action' needs to be of type: "), + ("red_uses_zero_day_action", "fail", + "'red_uses_zero_day_action' needs to be of type: "), + ("red_uses_basic_attack_action", "fail", + "'red_uses_basic_attack_action' needs to be of type: "), + ("red_uses_do_nothing_action", "fail", + "'red_uses_do_nothing_action' needs to be of type: "), + ("red_uses_move_action", "fail", + "'red_uses_move_action' needs to be of type: "), + ("red_chooses_target_at_random", "fail", + "'red_chooses_target_at_random' needs to be of type: "), + ("red_prioritises_connected_nodes", "fail", + "'red_prioritises_connected_nodes' needs to be of type: "), + ("red_prioritises_un_connected_nodes", "fail", + "'red_prioritises_un_connected_nodes' needs to be of type: "), + ("red_prioritises_vulnerable_nodes", "fail", + "'red_prioritises_vulnerable_nodes' needs to be of type: "), + ("red_prioritises_resilient_nodes", "fail", + "'red_prioritises_resilient_nodes' needs to be of type: "), + ("red_can_naturally_spread", "fail", + "'red_can_naturally_spread' needs to be of type: ") + ] +) +def test_invalid_config_type(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + RedAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # BETWEEN 0 and 1 + ("red_skill", 2, + "'red_skill' Needs to have a value less than: 1 (inclusive)"), + ("red_skill", -1, + "'red_skill' Needs to have a value greater than: 0 (inclusive)"), + ("chance_for_red_to_spread", 2, + "'chance_for_red_to_spread' Needs to have a value less than: 1 (inclusive)"), + ("chance_for_red_to_spread", -1, + "'chance_for_red_to_spread' Needs to have a value greater than: 0 (inclusive)"), + ("chance_for_red_to_random_compromise", 2, + "'chance_for_red_to_random_compromise' Needs to have a value less than: 1 (inclusive)"), + ("chance_for_red_to_random_compromise", -1, + "'chance_for_red_to_random_compromise' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_spread_to_connected_node", 2, + "'chance_to_spread_to_connected_node' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_spread_to_connected_node", -1, + "'chance_to_spread_to_connected_node' Needs to have a value greater than: 0 (inclusive)"), + ("chance_to_spread_to_unconnected_node", 2, + "'chance_to_spread_to_unconnected_node' Needs to have a value less than: 1 (inclusive)"), + ("chance_to_spread_to_unconnected_node", -1, + "'chance_to_spread_to_unconnected_node' Needs to have a value greater than: 0 (inclusive)"), + + # GREATER THAN 0 + ("spread_action_likelihood", -1, + "'spread_action_likelihood' Needs to have a value greater than: 0 (not inclusive)"), + ("random_infect_action_likelihood", -1, + "'random_infect_action_likelihood' Needs to have a value greater than: 0 (not inclusive)"), + ("basic_attack_action_likelihood", -1, + "'basic_attack_action_likelihood' Needs to have a value greater than: 0 (not inclusive)"), + ("do_nothing_action_likelihood", -1, + "'do_nothing_action_likelihood' Needs to have a value greater than: 0 (not inclusive)"), + ("move_action_likelihood", -1, + "'move_action_likelihood' Needs to have a value greater than: 0 (not inclusive)"), + + # GREATER THAN OR EQUAL TO 0 + ("zero_day_start_amount", -1, + "'zero_day_start_amount' Needs to have a value greater than: 0 (inclusive)"), + ("days_required_for_zero_day", -1, + "'days_required_for_zero_day' Needs to have a value greater than: 0 (inclusive)"), + + ] +) +def test_invalid_config_range(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + RedAgentConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err diff --git a/tests/unit_tests/config/config_test_utils.py b/tests/unit_tests/config/config_test_utils.py new file mode 100644 index 00000000..4940de1a --- /dev/null +++ b/tests/unit_tests/config/config_test_utils.py @@ -0,0 +1,10 @@ +import yaml +from yaml.loader import SafeLoader + + +def read_yaml_file(yaml_location: str): + try: + with open(yaml_location) as f: + return yaml.load(f, Loader=SafeLoader) + except FileNotFoundError as e: + raise e diff --git a/tests/unit_tests/config/environment/__init__.py b/tests/unit_tests/config/environment/__init__.py index e69de29b..71585c3b 100644 --- a/tests/unit_tests/config/environment/__init__.py +++ b/tests/unit_tests/config/environment/__init__.py @@ -0,0 +1,22 @@ +from typing import Final +import os +from pathlib import Path +from typing import Final + +from tests import TEST_CONFIG_PATH + +TEST_GAME_RULES_CONFIG_PATH: Final[Path] = Path( + os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_game_rules_config.yaml") +) + +TEST_OBSERVATION_SPACE_CONFIG_PATH: Final[Path] = Path( + os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_observation_space_config.yaml") +) + +TEST_RESET_CONFIG_PATH: Final[Path] = Path( + os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_reset_config.yaml") +) + +TEST_REWARDS_CONFIG_PATH: Final[Path] = Path( + os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_rewards_config.yaml") +) diff --git a/yawning_titan/config/_package_data/game_modes/default_game_mode.yaml b/yawning_titan/config/_package_data/game_modes/default_game_mode.yaml index 6d8c24b1..8d599428 100644 --- a/yawning_titan/config/_package_data/game_modes/default_game_mode.yaml +++ b/yawning_titan/config/_package_data/game_modes/default_game_mode.yaml @@ -179,6 +179,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/yawning_titan/config/_package_data/game_modes/low_skill_red_with_random_infection_perfect_detection.yaml b/yawning_titan/config/_package_data/game_modes/low_skill_red_with_random_infection_perfect_detection.yaml index 526b0b65..04be2910 100644 --- a/yawning_titan/config/_package_data/game_modes/low_skill_red_with_random_infection_perfect_detection.yaml +++ b/yawning_titan/config/_package_data/game_modes/low_skill_red_with_random_infection_perfect_detection.yaml @@ -147,6 +147,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/yawning_titan/config/_package_data/game_modes/multiple_high_value_targets.yaml b/yawning_titan/config/_package_data/game_modes/multiple_high_value_targets.yaml index 6acfdcb6..72422c7b 100644 --- a/yawning_titan/config/_package_data/game_modes/multiple_high_value_targets.yaml +++ b/yawning_titan/config/_package_data/game_modes/multiple_high_value_targets.yaml @@ -179,6 +179,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/yawning_titan/config/agents/blue_agent_config.py b/yawning_titan/config/agents/blue_agent_config.py index 2bc82e33..a09a3f57 100644 --- a/yawning_titan/config/agents/blue_agent_config.py +++ b/yawning_titan/config/agents/blue_agent_config.py @@ -1,3 +1,4 @@ +from __future__ import annotations from dataclasses import dataclass from typing import Any, Dict diff --git a/yawning_titan/config/environment/game_rules_config.py b/yawning_titan/config/environment/game_rules_config.py index 94f27e90..5a018d0b 100644 --- a/yawning_titan/config/environment/game_rules_config.py +++ b/yawning_titan/config/environment/game_rules_config.py @@ -13,6 +13,9 @@ class GameRulesConfig(ConfigGroupABC): Class that validates and stores Game Rules Configuration """ + gr_min_number_of_network_nodes: int + """The minimum number of nodes the game mode will be allowed to run on""" + gr_node_vuln_lower: float """Lower bound of the node vulnerability""" @@ -61,19 +64,12 @@ class GameRulesConfig(ConfigGroupABC): @classmethod def create( cls, - settings: Dict[str, Any], - network_config: NetworkConfig + settings: Dict[str, Any] ) -> GameRulesConfig: - cls._validate(settings, network_config) - - if not network_config.high_value_targets: - gr_number_of_high_value_targets = settings[ - "number_of_high_value_targets" - ] - else: - gr_number_of_high_value_targets = len(network_config.matrix) + cls._validate(settings) game_rule_config = GameRulesConfig( + gr_min_number_of_network_nodes=settings["min_number_of_network_nodes"], gr_node_vuln_lower=settings[ "node_vulnerability_lower_bound" ], @@ -90,7 +86,7 @@ def create( gr_loss_pc_node_compromised_pc=settings[ "percentage_of_nodes_compromised_equals_loss" ], - gr_number_of_high_value_targets=gr_number_of_high_value_targets, + gr_number_of_high_value_targets=["number_of_high_value_targets"], gr_loss_hvt=settings["lose_when_high_value_target_lost"], gr_loss_hvt_random_placement=settings[ "choose_high_value_targets_placement_at_random" @@ -117,12 +113,8 @@ def create( @classmethod def _validate( cls, - data: dict, - network_config: NetworkConfig + data: dict ): - high_value_targets = network_config.high_value_targets - number_of_nodes = len(network_config.matrix) - # data is int or float for name in [ "node_vulnerability_lower_bound", @@ -137,23 +129,19 @@ def _validate( if data["node_vulnerability_lower_bound"] > data["node_vulnerability_upper_bound"]: raise ValueError( "'node_vulnerability_lower_bound', 'node_vulnerability_upper_bound' -> The lower bound for the node vulnerabilities should be less than the upper bound" - # noqa ) check_type(data, "max_steps", [int]) check_type(data, "number_of_entry_nodes", [int]) check_type(data, "grace_period_length", [int]) - # ignore if high value targets passed in - if not high_value_targets: - check_type(data, "number_of_high_value_targets", [int]) - check_within_range(data, "number_of_high_value_targets", 1, number_of_nodes, True, True) - else: - # make sure the passed high value targets do not exceed the number of nodes in network - check_within_range({'hvt_length': len(high_value_targets)}, "hvt_length", 1, number_of_nodes, True, - True) + check_type(data, "min_number_of_network_nodes", [int]) + check_type(data, "number_of_high_value_targets", [int]) + # make sure high value targets is not more than the number of minimum number of nodes in network + check_within_range(data, "number_of_high_value_targets", 1, data["min_number_of_network_nodes"], True, True) check_within_range(data, "grace_period_length", 0, 100, True, True) check_within_range(data, "max_steps", 0, 10000000, False, True) - check_within_range(data, "number_of_entry_nodes", 0, number_of_nodes, False, True) + # make sure entry nodes is not more than the number of minimum number of nodes in network + check_within_range(data, "number_of_entry_nodes", 0, data["min_number_of_network_nodes"], False, True) # data is boolean for name in [ diff --git a/yawning_titan/config/game_config/game_mode_config.py b/yawning_titan/config/game_config/game_mode_config.py index bd477234..3cd3aeab 100644 --- a/yawning_titan/config/game_config/game_mode_config.py +++ b/yawning_titan/config/game_config/game_mode_config.py @@ -1,3 +1,4 @@ +from __future__ import annotations from dataclasses import dataclass from logging import getLogger @@ -56,3 +57,32 @@ class GameModeConfig: Is true if the timestep data is output to JSON """ + @classmethod + def create( + cls, + config_path=None + ) -> GameModeConfig: + """ + Creates an instance of the GameModeConfig class + """ + # opens the fle the user has specified to be the location of the settings + if not config_path: + settings_path = default_game_mode_path() + try: + with open(settings_path) as f: + settings = yaml.load(f, Loader=SafeLoader) + except FileNotFoundError as e: + msg = f"Configuration file does not exist: {settings_path}" + print(msg) # TODO: Remove once proper logging is setup + _LOGGER.critical(msg, exc_info=True) + raise e + + return GameModeConfig( + red_agent_config=RedAgentConfig.create(settings["RED"]), + blue_agent_config=BlueAgentConfig.create(settings["BLUE"]), + observation_space_config=ObservationSpaceConfig.create(settings["OBSERVATION_SPACE"]), + game_rules_config=GameRulesConfig.create(settings=settings["GAME_RULES"]), + reset_config=ResetConfig.create(settings["RESET"]), + rewards_config=RewardsConfig.create(settings["REWARDS"]), + output_timestep_data_to_json=True + ) diff --git a/yawning_titan/config/game_config/game_mode_config_builder.py b/yawning_titan/config/game_config/game_mode_config_builder.py deleted file mode 100644 index ff603d62..00000000 --- a/yawning_titan/config/game_config/game_mode_config_builder.py +++ /dev/null @@ -1,54 +0,0 @@ -from logging import getLogger - -from yawning_titan.config.environment.network_config import NetworkConfig -from yawning_titan.config.game_config.game_mode_config import GameModeConfig - -import yaml -from yaml import SafeLoader - -from yawning_titan.config.agents.red_agent_config import RedAgentConfig -from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig -from yawning_titan.config.environment.game_rules_config import GameRulesConfig -from yawning_titan.config.environment.observation_space_config import ObservationSpaceConfig -from yawning_titan.config.environment.reset_config import ResetConfig -from yawning_titan.config.environment.rewards_config import RewardsConfig -from yawning_titan.config.game_modes import default_game_mode_path - -_LOGGER = getLogger(__name__) - - -class GameModeConfigBuilder: - """ - Class Builder for GameModeConfig - """ - - @classmethod - def create( - cls, - network_config: NetworkConfig, - config_path=None - ) -> GameModeConfig: - """ - Creates an instance of the GameModeConfig class - """ - # opens the fle the user has specified to be the location of the settings - if not config_path: - settings_path = default_game_mode_path() - try: - with open(settings_path) as f: - settings = yaml.load(f, Loader=SafeLoader) - except FileNotFoundError as e: - msg = f"Configuration file does not exist: {settings_path}" - print(msg) # TODO: Remove once proper logging is setup - _LOGGER.critical(msg, exc_info=True) - raise e - - return GameModeConfig( - red_agent_config=RedAgentConfig.create(settings["RED"]), - blue_agent_config=BlueAgentConfig.create(settings["BLUE"]), - observation_space_config=ObservationSpaceConfig.create(settings["OBSERVATION_SPACE"]), - game_rules_config=GameRulesConfig.create(settings=settings["GAME_RULES"], network_config=network_config), - reset_config=ResetConfig.create(settings["RESET"]), - rewards_config=RewardsConfig.create(settings["REWARDS"]), - output_timestep_data_to_json=True - ) diff --git a/yawning_titan/envs/generic/core/network_interface.py b/yawning_titan/envs/generic/core/network_interface.py index 7cedc272..e4d53ae9 100644 --- a/yawning_titan/envs/generic/core/network_interface.py +++ b/yawning_titan/envs/generic/core/network_interface.py @@ -63,6 +63,8 @@ def __init__( _LOGGER.critical(msg, exc_info=True) raise e + # TODO - store network configuration in NetworkConfig Class + self.matrix = matrix number_of_nodes = len(matrix) From f56b79ae3aed458aebd452740c63e3ac9f049cb4 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 7 Nov 2022 13:21:17 +0000 Subject: [PATCH 4/7] AIDT-67: more tests + adjusting game rules validation --- ...=> complete_observation_space_config.yaml} | 0 tests/unit_tests/config/config_test_utils.py | 4 +- .../environment/test_game_rules_config.py | 148 ++++++++++++++++++ .../test_observation_space_config.py | 87 ++++++++++ .../config/environment/test_reset_config.py | 45 ++++++ .../config/environment/test_rewards_config.py | 67 ++++++++ .../config/environment/game_rules_config.py | 16 +- .../config/environment/rewards_config.py | 2 + 8 files changed, 354 insertions(+), 15 deletions(-) rename tests/test_configs/config_sections/{complete_observation_config.yaml => complete_observation_space_config.yaml} (100%) diff --git a/tests/test_configs/config_sections/complete_observation_config.yaml b/tests/test_configs/config_sections/complete_observation_space_config.yaml similarity index 100% rename from tests/test_configs/config_sections/complete_observation_config.yaml rename to tests/test_configs/config_sections/complete_observation_space_config.yaml diff --git a/tests/unit_tests/config/config_test_utils.py b/tests/unit_tests/config/config_test_utils.py index 4940de1a..62cc1607 100644 --- a/tests/unit_tests/config/config_test_utils.py +++ b/tests/unit_tests/config/config_test_utils.py @@ -1,8 +1,10 @@ +from pathlib import Path + import yaml from yaml.loader import SafeLoader -def read_yaml_file(yaml_location: str): +def read_yaml_file(yaml_location: Path): try: with open(yaml_location) as f: return yaml.load(f, Loader=SafeLoader) diff --git a/tests/unit_tests/config/environment/test_game_rules_config.py b/tests/unit_tests/config/environment/test_game_rules_config.py index e69de29b..775f3572 100644 --- a/tests/unit_tests/config/environment/test_game_rules_config.py +++ b/tests/unit_tests/config/environment/test_game_rules_config.py @@ -0,0 +1,148 @@ +from typing import Dict, Any + +import pytest + +from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests.unit_tests.config.environment import TEST_GAME_RULES_CONFIG_PATH +from yawning_titan.config.environment.game_rules_config import GameRulesConfig + + +def get_config_dict() -> Dict: + return read_yaml_file(TEST_GAME_RULES_CONFIG_PATH) + + +def test_read_valid_config(): + game_rules = GameRulesConfig.create(get_config_dict()) + + assert game_rules.gr_min_number_of_network_nodes == 18 + + assert game_rules.gr_node_vuln_lower == 0.2 + + assert game_rules.gr_node_vuln_upper == 0.8 + + assert game_rules.gr_max_steps == 1000 + + assert game_rules.gr_loss_total_compromise is False + + assert game_rules.gr_loss_pc_nodes_compromised is False + + assert game_rules.gr_loss_pc_node_compromised_pc == 0.8 + + assert game_rules.gr_number_of_high_value_targets == 1 + + assert game_rules.gr_loss_hvt is True + + assert game_rules.gr_loss_hvt_random_placement is False + + assert game_rules.gr_loss_hvt_furthest_away is True + + assert game_rules.gr_random_entry_nodes is True + + assert game_rules.gr_num_entry_nodes == 3 + + assert game_rules.gr_prefer_central_entry is True + + assert game_rules.gr_prefer_edge_nodes is False + + assert game_rules.gr_grace_period == 0 + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # INT/FLOAT + ("node_vulnerability_lower_bound", True, + "'node_vulnerability_lower_bound' needs to be of type: or " + ""), + ("node_vulnerability_upper_bound", True, + "'node_vulnerability_upper_bound' needs to be of type: or " + ""), + ("percentage_of_nodes_compromised_equals_loss", True, + "'percentage_of_nodes_compromised_equals_loss' needs to be of type: or " + ""), + # INT + ("min_number_of_network_nodes", 0.5, + "'min_number_of_network_nodes' needs to be of type: "), + ("max_steps", 0.5, + "'max_steps' needs to be of type: "), + ("number_of_high_value_targets", 0.5, + "'number_of_high_value_targets' needs to be of type: "), + ("number_of_entry_nodes", 0.5, + "'number_of_entry_nodes' needs to be of type: "), + ("grace_period_length", 0.5, + "'grace_period_length' needs to be of type: "), + # BOOLEAN + ("lose_when_all_nodes_lost", 0.5, + "'lose_when_all_nodes_lost' needs to be of type: "), + ("lose_when_n_percent_of_nodes_lost", 0.5, + "'lose_when_n_percent_of_nodes_lost' needs to be of type: "), + ("lose_when_high_value_target_lost", 0.5, + "'lose_when_high_value_target_lost' needs to be of type: "), + ("choose_high_value_targets_placement_at_random", 0.5, + "'choose_high_value_targets_placement_at_random' needs to be of type: "), + ("choose_high_value_targets_furthest_away_from_entry", 0.5, + "'choose_high_value_targets_furthest_away_from_entry' needs to be of type: "), + ("choose_entry_nodes_randomly", 0.5, + "'choose_entry_nodes_randomly' needs to be of type: "), + ("prefer_central_nodes_for_entry_nodes", 0.5, + "'prefer_central_nodes_for_entry_nodes' needs to be of type: "), + ("prefer_edge_nodes_for_entry_nodes", 0.5, + "'prefer_edge_nodes_for_entry_nodes' needs to be of type: "), + ] +) +def test_invalid_config_type(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + GameRulesConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # BETWEEN 0 AND 1 + ("node_vulnerability_lower_bound", -0.5, + "'node_vulnerability_lower_bound' Needs to have a value greater than: 0 (inclusive)"), + ("node_vulnerability_lower_bound", 1.1, + "'node_vulnerability_lower_bound' Needs to have a value less than: 1 (inclusive)"), + + # MORE THAN OR EQUAL TO 0 BUT LESS THAN MIN NUM OF NODES + ("number_of_high_value_targets", -1, + "'number_of_high_value_targets' Needs to have a value greater than: 0 (inclusive)"), + ("number_of_high_value_targets", 19, + "'number_of_high_value_targets' Needs to have a value less than: 18 (inclusive)"), + ("number_of_entry_nodes", -1, + "'number_of_entry_nodes' Needs to have a value greater than: 0 (not inclusive)"), + ("number_of_entry_nodes", 19, + "'number_of_entry_nodes' Needs to have a value less than: 18 (inclusive)"), + + # BETWEEN 0 AND 100 + ("grace_period_length", -1, + "'grace_period_length' Needs to have a value greater than: 0 (inclusive)"), + ("grace_period_length", 101, + "'grace_period_length' Needs to have a value less than: 100 (inclusive)"), + + # MAX STEPS + ("max_steps", -1, + "'max_steps' Needs to have a value greater than: 0 (not inclusive)"), + ("max_steps", 10000001, + "'max_steps' Needs to have a value less than: 10000000 (inclusive)"), + ] +) +def test_invalid_config_range(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + GameRulesConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err diff --git a/tests/unit_tests/config/environment/test_observation_space_config.py b/tests/unit_tests/config/environment/test_observation_space_config.py index e69de29b..bc8c2643 100644 --- a/tests/unit_tests/config/environment/test_observation_space_config.py +++ b/tests/unit_tests/config/environment/test_observation_space_config.py @@ -0,0 +1,87 @@ +from typing import Dict, Any + +import pytest + +from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests.unit_tests.config.environment import TEST_OBSERVATION_SPACE_CONFIG_PATH +from yawning_titan.config.environment.observation_space_config import ObservationSpaceConfig + + +def get_config_dict() -> Dict: + return read_yaml_file(TEST_OBSERVATION_SPACE_CONFIG_PATH) + + +def test_read_valid_config(): + obs_space = ObservationSpaceConfig.create(get_config_dict()) + + assert obs_space.obs_compromised_status is True + + assert obs_space.obs_node_vuln_status is True + + assert obs_space.obs_node_connections is True + + assert obs_space.obs_avg_vuln is False + + assert obs_space.obs_graph_connectivity is True + + assert obs_space.obs_attack_sources is True + + assert obs_space.obs_attack_targets is True + + assert obs_space.obs_special_nodes is True + + assert obs_space.obs_red_agent_skill is True + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + ("compromised_status", "fail", + "'compromised_status' needs to be of type: "), + ("vulnerabilities", "fail", + "'vulnerabilities' needs to be of type: "), + ("node_connections", "fail", + "'node_connections' needs to be of type: "), + ("average_vulnerability", "fail", + "'average_vulnerability' needs to be of type: "), + ("graph_connectivity", "fail", + "'graph_connectivity' needs to be of type: "), + ("attacking_nodes", "fail", + "'attacking_nodes' needs to be of type: "), + ("attacked_nodes", "fail", + "'attacked_nodes' needs to be of type: "), + ("special_nodes", "fail", + "'special_nodes' needs to be of type: "), + ("red_agent_skill", "fail", + "'red_agent_skill' needs to be of type: "), + ] +) +def test_invalid_config_type(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + ObservationSpaceConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err + + +def test_at_least_one_option_selected(): + with pytest.raises(ValueError) as err_info: + ObservationSpaceConfig.create({ + "compromised_status": False, + "vulnerabilities": False, + "node_connections": False, + "average_vulnerability": False, + "graph_connectivity": False, + "attacking_nodes": False, + "attacked_nodes": False, + "special_nodes": False, + "red_agent_skill": False + }) + + # assert that the error message is as expected + assert err_info.value.args[0] == "At least one option from OBSERVATION_SPACE must be enabled. The observation space must contain at least one item" diff --git a/tests/unit_tests/config/environment/test_reset_config.py b/tests/unit_tests/config/environment/test_reset_config.py index e69de29b..b2c757de 100644 --- a/tests/unit_tests/config/environment/test_reset_config.py +++ b/tests/unit_tests/config/environment/test_reset_config.py @@ -0,0 +1,45 @@ +from typing import Dict, Any + +import pytest + +from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests.unit_tests.config.environment import TEST_RESET_CONFIG_PATH +from yawning_titan.config.environment.reset_config import ResetConfig + + +def get_config_dict() -> Dict: + return read_yaml_file(TEST_RESET_CONFIG_PATH) + + +def test_read_valid_config(): + reset_config = ResetConfig.create(get_config_dict()) + + assert reset_config.reset_random_vulns is False + + assert reset_config.reset_move_hvt is True + + assert reset_config.reset_move_entry_nodes is True + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + ("randomise_vulnerabilities_on_reset", "fail", + "'randomise_vulnerabilities_on_reset' needs to be of type: "), + ("choose_new_high_value_targets_on_reset", "fail", + "'choose_new_high_value_targets_on_reset' needs to be of type: "), + ("choose_new_entry_nodes_on_reset", "fail", + "'choose_new_entry_nodes_on_reset' needs to be of type: "), + ] +) +def test_invalid_config_type(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + ResetConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err diff --git a/tests/unit_tests/config/environment/test_rewards_config.py b/tests/unit_tests/config/environment/test_rewards_config.py index e69de29b..f91f33c6 100644 --- a/tests/unit_tests/config/environment/test_rewards_config.py +++ b/tests/unit_tests/config/environment/test_rewards_config.py @@ -0,0 +1,67 @@ +from typing import Dict, Any + +import pytest + +from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests.unit_tests.config.environment import TEST_REWARDS_CONFIG_PATH +from yawning_titan.config.environment.rewards_config import RewardsConfig + + +def get_config_dict() -> Dict: + return read_yaml_file(TEST_REWARDS_CONFIG_PATH) + + +def test_read_valid_config(): + rewards_config = RewardsConfig.create(get_config_dict()) + + assert rewards_config.reward_loss == -100 + + assert rewards_config.reward_success == 100 + + assert rewards_config.reward_end_multiplier is True + + assert rewards_config.reward_reduce_negative_rewards is True + + assert rewards_config.reward_function == "standard_rewards" + + +@pytest.mark.parametrize( + ("config_item_to_test", "config_value", "expected_err"), + [ + # INT/FLOAT + ("rewards_for_loss", "fail", + "'rewards_for_loss' needs to be of type: or "), + ("rewards_for_reaching_max_steps", "fail", + "'rewards_for_reaching_max_steps' needs to be of type: or "), + + # BOOLEAN + ("end_rewards_are_multiplied_by_end_state", "fail", + "'end_rewards_are_multiplied_by_end_state' needs to be of type: "), + ("reduce_negative_rewards_for_closer_fails", "fail", + "'reduce_negative_rewards_for_closer_fails' needs to be of type: "), + ] +) +def test_invalid_config_type(config_item_to_test: str, config_value: Any, expected_err: str): + conf: Dict = get_config_dict() + + # set value + conf[config_item_to_test] = config_value + + with pytest.raises(ValueError) as err_info: + RewardsConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == expected_err + + +def test_invalid_reward_function_type(): + conf: Dict = get_config_dict() + + # set value + conf["reward_function"] = True + + with pytest.raises(TypeError) as err_info: + RewardsConfig.create(conf) + + # assert that the error message is as expected + assert err_info.value.args[0] == "hasattr(): attribute name must be string" diff --git a/yawning_titan/config/environment/game_rules_config.py b/yawning_titan/config/environment/game_rules_config.py index 5a018d0b..7e95fce7 100644 --- a/yawning_titan/config/environment/game_rules_config.py +++ b/yawning_titan/config/environment/game_rules_config.py @@ -86,7 +86,7 @@ def create( gr_loss_pc_node_compromised_pc=settings[ "percentage_of_nodes_compromised_equals_loss" ], - gr_number_of_high_value_targets=["number_of_high_value_targets"], + gr_number_of_high_value_targets=settings["number_of_high_value_targets"], gr_loss_hvt=settings["lose_when_high_value_target_lost"], gr_loss_hvt_random_placement=settings[ "choose_high_value_targets_placement_at_random" @@ -136,7 +136,7 @@ def _validate( check_type(data, "min_number_of_network_nodes", [int]) check_type(data, "number_of_high_value_targets", [int]) # make sure high value targets is not more than the number of minimum number of nodes in network - check_within_range(data, "number_of_high_value_targets", 1, data["min_number_of_network_nodes"], True, True) + check_within_range(data, "number_of_high_value_targets", 0, data["min_number_of_network_nodes"], True, True) check_within_range(data, "grace_period_length", 0, 100, True, True) check_within_range(data, "max_steps", 0, 10000000, False, True) @@ -181,7 +181,6 @@ def _validate( if data["lose_when_high_value_target_lost"]: # if there is no way to set high value targets if ( - not high_value_targets and not data["choose_high_value_targets_placement_at_random"] and not data["choose_high_value_targets_furthest_away_from_entry"] ): @@ -198,17 +197,6 @@ def _validate( "'choose_high_value_targets_placement_at_random', 'choose_high_value_targets_furthest_away_from_entry' -> Only one method of selecting a high value target should be selected" # noqa ) - # if high value targets are set and these configurations are also set - if ( - high_value_targets and - (data["choose_high_value_targets_placement_at_random"] - or data["choose_high_value_targets_furthest_away_from_entry"]) - ): - raise ValueError( - "Provided high value targets: " + str( - high_value_targets) + " 'choose_high_value_targets_placement_at_random', 'choose_high_value_targets_furthest_away_from_entry' -> Only one method of selecting a high value target should be selected" - # noqa - ) if data["grace_period_length"] > data["max_steps"]: raise ValueError( diff --git a/yawning_titan/config/environment/rewards_config.py b/yawning_titan/config/environment/rewards_config.py index 655d85ed..0fe157e2 100644 --- a/yawning_titan/config/environment/rewards_config.py +++ b/yawning_titan/config/environment/rewards_config.py @@ -44,6 +44,8 @@ def create( cls, settings: Dict[str, Any] ) -> RewardsConfig: + cls._validate(settings) + rewards = RewardsConfig( reward_loss=settings["rewards_for_loss"], reward_success=settings["rewards_for_reaching_max_steps"], From f5407882e72d4b25d9a25ed1fe419994b52d6ce8 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 7 Nov 2022 14:01:26 +0000 Subject: [PATCH 5/7] AIDT-67: testing GameModeConfig --- tests/test_configs/base_config.yaml | 2 + tests/test_configs/everything_guaranteed.yaml | 2 + .../high_value_target_provided.yaml | 2 + tests/test_configs/new_entry_nodes.yaml | 2 + tests/test_configs/new_high_value_target.yaml | 2 + tests/test_configs/red_config_test_1.yaml | 2 + tests/test_configs/red_config_test_2.yaml | 2 + tests/test_configs/red_config_test_3.yaml | 2 + tests/test_configs/red_config_test_4.yaml | 2 + tests/test_configs/red_config_test_5.yaml | 2 + .../red_config_test_broken_1.yaml | 2 + .../red_config_test_broken_2.yaml | 2 + .../red_config_test_broken_3.yaml | 2 + tests/test_configs/spreading_config.yaml | 2 + .../too_many_high_value_targets_provided.yaml | 2 + .../game_config/test_game_mode_config.py | 58 +++++++++++++++++++ .../test_game_mode_config_builder.py | 0 .../config/game_config/game_mode_config.py | 9 ++- 18 files changed, 95 insertions(+), 2 deletions(-) delete mode 100644 tests/unit_tests/config/game_config/test_game_mode_config_builder.py diff --git a/tests/test_configs/base_config.yaml b/tests/test_configs/base_config.yaml index 22b40a0d..00ffffd6 100644 --- a/tests/test_configs/base_config.yaml +++ b/tests/test_configs/base_config.yaml @@ -176,6 +176,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/everything_guaranteed.yaml b/tests/test_configs/everything_guaranteed.yaml index db7f8e5b..66af195c 100644 --- a/tests/test_configs/everything_guaranteed.yaml +++ b/tests/test_configs/everything_guaranteed.yaml @@ -176,6 +176,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/high_value_target_provided.yaml b/tests/test_configs/high_value_target_provided.yaml index d3b724b7..64556d57 100644 --- a/tests/test_configs/high_value_target_provided.yaml +++ b/tests/test_configs/high_value_target_provided.yaml @@ -176,6 +176,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/new_entry_nodes.yaml b/tests/test_configs/new_entry_nodes.yaml index cd8bc0d4..602398f3 100644 --- a/tests/test_configs/new_entry_nodes.yaml +++ b/tests/test_configs/new_entry_nodes.yaml @@ -179,6 +179,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/new_high_value_target.yaml b/tests/test_configs/new_high_value_target.yaml index 854cdfb8..82bc6543 100644 --- a/tests/test_configs/new_high_value_target.yaml +++ b/tests/test_configs/new_high_value_target.yaml @@ -179,6 +179,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_1.yaml b/tests/test_configs/red_config_test_1.yaml index e1bcb406..957b6e81 100644 --- a/tests/test_configs/red_config_test_1.yaml +++ b/tests/test_configs/red_config_test_1.yaml @@ -149,6 +149,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_2.yaml b/tests/test_configs/red_config_test_2.yaml index f775935f..85d0bc10 100644 --- a/tests/test_configs/red_config_test_2.yaml +++ b/tests/test_configs/red_config_test_2.yaml @@ -149,6 +149,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: False GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_3.yaml b/tests/test_configs/red_config_test_3.yaml index 036d30bf..357efe91 100644 --- a/tests/test_configs/red_config_test_3.yaml +++ b/tests/test_configs/red_config_test_3.yaml @@ -151,6 +151,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_4.yaml b/tests/test_configs/red_config_test_4.yaml index 2be7b030..d4277452 100644 --- a/tests/test_configs/red_config_test_4.yaml +++ b/tests/test_configs/red_config_test_4.yaml @@ -151,6 +151,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_5.yaml b/tests/test_configs/red_config_test_5.yaml index 5c4c93e9..8754daed 100644 --- a/tests/test_configs/red_config_test_5.yaml +++ b/tests/test_configs/red_config_test_5.yaml @@ -151,6 +151,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.1 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_broken_1.yaml b/tests/test_configs/red_config_test_broken_1.yaml index 0403c5ce..44db1dcb 100644 --- a/tests/test_configs/red_config_test_broken_1.yaml +++ b/tests/test_configs/red_config_test_broken_1.yaml @@ -149,6 +149,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.7 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_broken_2.yaml b/tests/test_configs/red_config_test_broken_2.yaml index dd4131d0..ae04ebc4 100644 --- a/tests/test_configs/red_config_test_broken_2.yaml +++ b/tests/test_configs/red_config_test_broken_2.yaml @@ -149,6 +149,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.7 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/red_config_test_broken_3.yaml b/tests/test_configs/red_config_test_broken_3.yaml index eddb0b11..fd922e82 100644 --- a/tests/test_configs/red_config_test_broken_3.yaml +++ b/tests/test_configs/red_config_test_broken_3.yaml @@ -150,6 +150,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.7 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/spreading_config.yaml b/tests/test_configs/spreading_config.yaml index 01c17aa7..575fde05 100644 --- a/tests/test_configs/spreading_config.yaml +++ b/tests/test_configs/spreading_config.yaml @@ -179,6 +179,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/test_configs/too_many_high_value_targets_provided.yaml b/tests/test_configs/too_many_high_value_targets_provided.yaml index c65253c2..781550cd 100644 --- a/tests/test_configs/too_many_high_value_targets_provided.yaml +++ b/tests/test_configs/too_many_high_value_targets_provided.yaml @@ -179,6 +179,8 @@ BLUE: relocating_deceptive_nodes_generates_a_new_node: True GAME_RULES: + # Minimum number of nodes the network this game mode is allowed to run on + min_number_of_network_nodes: 18 # A lower vulnerability means that a node is less likely to be compromised node_vulnerability_lower_bound: 0.2 # A higher vulnerability means that a node is more vulnerable diff --git a/tests/unit_tests/config/game_config/test_game_mode_config.py b/tests/unit_tests/config/game_config/test_game_mode_config.py index e69de29b..ff037dda 100644 --- a/tests/unit_tests/config/game_config/test_game_mode_config.py +++ b/tests/unit_tests/config/game_config/test_game_mode_config.py @@ -0,0 +1,58 @@ +import os +from pathlib import Path +from typing import Dict + +import pytest + +from tests import TEST_CONFIG_PATH +from tests.unit_tests.config.config_test_utils import read_yaml_file +from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig +from yawning_titan.config.agents.red_agent_config import RedAgentConfig +from yawning_titan.config.environment.game_rules_config import GameRulesConfig +from yawning_titan.config.environment.observation_space_config import ObservationSpaceConfig +from yawning_titan.config.environment.reset_config import ResetConfig +from yawning_titan.config.environment.rewards_config import RewardsConfig +from yawning_titan.config.game_config.game_mode_config import GameModeConfig +from yawning_titan.config.game_modes import default_game_mode_path + + +def get_config_dict() -> Dict: + return read_yaml_file(Path(os.path.join(TEST_CONFIG_PATH, "base_config.yaml"))) + + +def get_default_config_dict() -> Dict: + return read_yaml_file(default_game_mode_path()) + + +def test_read_valid_path_and_valid_config(): + game_mode = GameModeConfig.create(os.path.join(TEST_CONFIG_PATH, "base_config.yaml")) + + assert game_mode.red_agent_config == RedAgentConfig.create(get_config_dict()["RED"]) + assert game_mode.blue_agent_config == BlueAgentConfig.create(get_config_dict()["BLUE"]) + assert game_mode.observation_space_config == ObservationSpaceConfig.create(get_config_dict()["OBSERVATION_SPACE"]) + assert game_mode.game_rules_config == GameRulesConfig.create(get_config_dict()["GAME_RULES"]) + assert game_mode.reset_config == ResetConfig.create(get_config_dict()["RESET"]) + assert game_mode.rewards_config == RewardsConfig.create(get_config_dict()["REWARDS"]) + assert game_mode.output_timestep_data_to_json == get_config_dict()["MISCELLANEOUS"]["output_timestep_data_to_json"] + + +def test_read_default_config(): + game_mode = GameModeConfig.create() + + assert game_mode.red_agent_config == RedAgentConfig.create(get_default_config_dict()["RED"]) + assert game_mode.blue_agent_config == BlueAgentConfig.create(get_default_config_dict()["BLUE"]) + assert game_mode.observation_space_config == ObservationSpaceConfig.create( + get_default_config_dict()["OBSERVATION_SPACE"]) + assert game_mode.game_rules_config == GameRulesConfig.create(get_default_config_dict()["GAME_RULES"]) + assert game_mode.reset_config == ResetConfig.create(get_default_config_dict()["RESET"]) + assert game_mode.rewards_config == RewardsConfig.create(get_default_config_dict()["REWARDS"]) + assert game_mode.output_timestep_data_to_json == get_default_config_dict()["MISCELLANEOUS"][ + "output_timestep_data_to_json"] + + +def test_invalid_path(): + with pytest.raises(FileNotFoundError) as err_info: + GameModeConfig.create(Path("fake_test_path")) + + # assert that the error message is as expected + assert err_info.value.args[1] == "No such file or directory" diff --git a/tests/unit_tests/config/game_config/test_game_mode_config_builder.py b/tests/unit_tests/config/game_config/test_game_mode_config_builder.py deleted file mode 100644 index e69de29b..00000000 diff --git a/yawning_titan/config/game_config/game_mode_config.py b/yawning_titan/config/game_config/game_mode_config.py index 3cd3aeab..e5aac90c 100644 --- a/yawning_titan/config/game_config/game_mode_config.py +++ b/yawning_titan/config/game_config/game_mode_config.py @@ -65,9 +65,14 @@ def create( """ Creates an instance of the GameModeConfig class """ - # opens the fle the user has specified to be the location of the settings + + # if no config provided, use default game mode if not config_path: settings_path = default_game_mode_path() + # otherwise, the settings path will be the path provided + else: + settings_path = config_path + try: with open(settings_path) as f: settings = yaml.load(f, Loader=SafeLoader) @@ -84,5 +89,5 @@ def create( game_rules_config=GameRulesConfig.create(settings=settings["GAME_RULES"]), reset_config=ResetConfig.create(settings["RESET"]), rewards_config=RewardsConfig.create(settings["REWARDS"]), - output_timestep_data_to_json=True + output_timestep_data_to_json=settings["MISCELLANEOUS"]["output_timestep_data_to_json"] ) From 17467314166b14942901dd0ea89790ac6f3675c3 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 7 Nov 2022 14:39:52 +0000 Subject: [PATCH 6/7] AIDT-67: moved network_config into its own directory + some test --- .../__init__.py} | 0 .../network_config/test_network_config.py | 36 +++++++++++++++++++ .../config/environment/game_rules_config.py | 1 - .../config/network_config/__init__.py | 0 .../network_config.py | 8 ++++- 5 files changed, 43 insertions(+), 2 deletions(-) rename tests/unit_tests/config/{environment/test_network_config.py => network_config/__init__.py} (100%) create mode 100644 tests/unit_tests/config/network_config/test_network_config.py create mode 100644 yawning_titan/config/network_config/__init__.py rename yawning_titan/config/{environment => network_config}/network_config.py (89%) diff --git a/tests/unit_tests/config/environment/test_network_config.py b/tests/unit_tests/config/network_config/__init__.py similarity index 100% rename from tests/unit_tests/config/environment/test_network_config.py rename to tests/unit_tests/config/network_config/__init__.py diff --git a/tests/unit_tests/config/network_config/test_network_config.py b/tests/unit_tests/config/network_config/test_network_config.py new file mode 100644 index 00000000..90310c71 --- /dev/null +++ b/tests/unit_tests/config/network_config/test_network_config.py @@ -0,0 +1,36 @@ +import warnings + +import numpy as np + +from yawning_titan.config.network_config.network_config import NetworkConfig +from yawning_titan.envs.generic.helpers import network_creator + +matrix, node_positions = network_creator.create_18_node_network() + + +def test_config_properties(): + network_config = NetworkConfig.create( + matrix=matrix, + positions=node_positions, + entry_nodes=["0"], + high_value_targets=["1"] + ) + + assert np.array_equal(network_config.matrix, matrix) is True + assert network_config.positions == node_positions + assert network_config.entry_nodes[0] == "0" + assert network_config.high_value_targets[0] == "1" + + +def test_hvn_entry_node_matching(): + with warnings.catch_warnings(record=True) as w: + network_config = NetworkConfig.create( + matrix=matrix, + positions=node_positions, + entry_nodes=["0"], + high_value_targets=["0"] + ) + + # check that a warning was raised that the entry nodes and high value targets intersect + assert str(w[0].message.args[ + 0]) == "Provided entry nodes and high value targets intersect and may cause the training to prematurely end" diff --git a/yawning_titan/config/environment/game_rules_config.py b/yawning_titan/config/environment/game_rules_config.py index 7e95fce7..c5330270 100644 --- a/yawning_titan/config/environment/game_rules_config.py +++ b/yawning_titan/config/environment/game_rules_config.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from typing import Dict, Any -from yawning_titan.config.environment.network_config import NetworkConfig from yawning_titan.config.game_config.config_group_class import ConfigGroupABC from yawning_titan.envs.generic.helpers.environment_input_validation import check_type, check_within_range diff --git a/yawning_titan/config/network_config/__init__.py b/yawning_titan/config/network_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yawning_titan/config/environment/network_config.py b/yawning_titan/config/network_config/network_config.py similarity index 89% rename from yawning_titan/config/environment/network_config.py rename to yawning_titan/config/network_config/network_config.py index a9631e94..20efbb23 100644 --- a/yawning_titan/config/environment/network_config.py +++ b/yawning_titan/config/network_config/network_config.py @@ -39,7 +39,13 @@ def create( vulnerabilities: Optional[Dict] = None, high_value_targets: Optional[List[str]] = None ): - cls._validate() + cls._validate( + matrix=matrix, + positions=positions, + entry_nodes=entry_nodes, + vulnerabilities=vulnerabilities, + high_value_targets=high_value_targets + ) network_config = NetworkConfig( matrix=matrix, From 1baf2910355fc0d617c4d8542bfa7abae669f5fb Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Tue, 8 Nov 2022 11:35:44 +0000 Subject: [PATCH 7/7] AIDT-67: remove partial configs and use base test config + code clean up --- tests/__init__.py | 4 + .../config => }/config_test_utils.py | 0 .../complete_blue_agent_config.yaml | 90 ------------------- .../complete_game_rules_config.yaml | 31 ------- .../complete_observation_space_config.yaml | 18 ---- .../complete_red_agent_config.yaml | 64 ------------- .../complete_reset_config.yaml | 3 - .../complete_rewards_config.yaml | 13 --- tests/unit_tests/config/agents/__init__.py | 13 --- .../config/agents/test_blue_agent_config.py | 29 +++--- .../config/agents/test_red_agent_config.py | 18 ++-- .../unit_tests/config/environment/__init__.py | 22 ----- .../environment/test_game_rules_config.py | 14 +-- .../test_observation_space_config.py | 8 +- .../config/environment/test_reset_config.py | 10 +-- .../config/environment/test_rewards_config.py | 8 +- .../game_config/test_game_mode_config.py | 8 +- .../network_config/test_network_config.py | 2 +- .../config/game_config/game_mode_config.py | 1 - 19 files changed, 52 insertions(+), 304 deletions(-) rename tests/{unit_tests/config => }/config_test_utils.py (100%) delete mode 100644 tests/test_configs/config_sections/complete_blue_agent_config.yaml delete mode 100644 tests/test_configs/config_sections/complete_game_rules_config.yaml delete mode 100644 tests/test_configs/config_sections/complete_observation_space_config.yaml delete mode 100644 tests/test_configs/config_sections/complete_red_agent_config.yaml delete mode 100644 tests/test_configs/config_sections/complete_reset_config.yaml delete mode 100644 tests/test_configs/config_sections/complete_rewards_config.yaml diff --git a/tests/__init__.py b/tests/__init__.py index 9864a818..8fd7c2be 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,3 +5,7 @@ TEST_CONFIG_PATH: Final[Path] = Path( os.path.join(Path(__file__).parent.resolve(), "test_configs") ) + +TEST_BASE_CONFIG_PATH = Path( + os.path.join(Path(__file__).parent.resolve(), "test_configs", "base_config.yaml") +) diff --git a/tests/unit_tests/config/config_test_utils.py b/tests/config_test_utils.py similarity index 100% rename from tests/unit_tests/config/config_test_utils.py rename to tests/config_test_utils.py diff --git a/tests/test_configs/config_sections/complete_blue_agent_config.yaml b/tests/test_configs/config_sections/complete_blue_agent_config.yaml deleted file mode 100644 index 81925944..00000000 --- a/tests/test_configs/config_sections/complete_blue_agent_config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# The max number of deceptive nodes that blue can place -max_number_deceptive_nodes: 2 -# Can discover the location an attack came from if the attack failed -can_discover_failed_attacks: True - - -# The blue agent does not have to have perfect detection. In these settings you can change how much information blue -# can gain from the red agents actions. There are two different pieces of information blue can get: intrusions and -# attacks. - -# --Intrusions-- -# An intrusion is when the red agent takes over a node and compromises it. You can change the chance that blue has to -# be able to detect this using the "chance_to_immediately_discover_intrusion". If blue does not detect an intrusion -# then it can use the scan action to try and discover these intrusions with "chance_to_discover_intrusion_on_scan". - -# There are also deceptive nodes that blue can place down. These nodes are used as detectors to inform blue when they -# are compromised. They should have a chance to detect of 1 so that they can detect everything (at the very least -# they should have a chance to detect higher than the normal chance to detect) but you can modify it if you so wish -# with "chance_to_immediately_discover_intrusion_deceptive_node" and "chance_to_discover_intrusion_on_scan_deceptive_node" - -# --Attacks-- -# Attacks are the actual attacks that the red agent does to compromise the nodes. For example you may be able to see -# that node 14 is compromised but using the attack detection, the blue agent may be able to see that it was node 12 -# that attacked node 14. You can modify the chance for blue to see attacks that failed, succeeded (and blue was able -# to detect that the node was compromised) and attacks that succeeded and the blue agent did not detect the intrusion. - -# Again there are settings to change the likelihood that a deceptive node can detect an attack. While this should -# remain at 1, it is open for you to change. - -# --INTRUSIONS-- -# -Standard Nodes- -# Chance for blue to discover a node that red has compromised the instant red compromises the node -chance_to_immediately_discover_intrusion: 0.5 -# When blue performs the scan action this is the chance that a red intrusion is discovered -chance_to_discover_intrusion_on_scan: 1 - -# -Deceptive Nodes- -# Chance for blue to discover a deceptive node that red has compromised the instant red compromises the node -chance_to_immediately_discover_intrusion_deceptive_node: 1 -# When blue uses the scan action what is the chance that blue will detect an intrusion in a deceptive node -chance_to_discover_intrusion_on_scan_deceptive_node: 1 - -# --ATTACKS-- -# -Standard Nodes- -# Chance for blue to discover information about a failed attack -chance_to_discover_failed_attack: 1 -# Can blue learn information about an attack that succeeds if the compromise is known -can_discover_succeeded_attacks_if_compromise_is_discovered: True -# Can blue learn information about an attack that succeeds if the compromise is NOT known -can_discover_succeeded_attacks_if_compromise_is_not_discovered: True -# Chance for blue to discover information about an attack that succeeded and the compromise was known -chance_to_discover_succeeded_attack_compromise_known: 1 -# Chance for blue to discover information about an attack that succeeded and the compromise was NOT known -chance_to_discover_succeeded_attack_compromise_not_known: 1 - -# -Deceptive Nodes- -# Chance to discover the location of a failed attack on a deceptive node -chance_to_discover_failed_attack_deceptive_node: 1 -# Chance to discover the location of a succeeded attack against a deceptive node -chance_to_discover_succeeded_attack_deceptive_node: 1 - - -# If blue fixes a node then the vulnerability score of that node increases -making_node_safe_modifies_vulnerability: False -# The amount that the vulnerability of a node changes when it is made safe -vulnerability_change_during_node_patch: 0.4 -# When fixing a node the vulnerability score is randomised -making_node_safe_gives_random_vulnerability: False - -# CHOOSE AT LEAST ONE OF THE FOLLOWING 8 ITEMS -# Blue picks a node and reduces the vulnerability score -blue_uses_reduce_vulnerability: False -# Blue picks a node and restores everything about the node to its state at the beginning of the game -blue_uses_restore_node: True -# Blue fixes a node but does not restore it to its initial state -blue_uses_make_node_safe: True -# Blue scans all of the nodes to try and detect any red intrusions -blue_uses_scan: True -# Blue disables all of the connections to and from a node -blue_uses_isolate_node: False -# Blue re-connects all of the connections to and from a node -blue_uses_reconnect_node: False -# Blue agent does nothing -blue_uses_do_nothing: False -# Blue agent can place down deceptive nodes. These nodes act as just another node in the network but have a different -# chance of spotting attacks and always show when they are compromised -blue_uses_deceptive_nodes: False -# When the blue agent places a deceptive node and it has none left in stock it will "pick up" the first deceptive node that it used and "relocate it" -# When relocating a node will the stats for the node (such as the vulnerability and compromised status) be re-generated as if adding a new node or will they carry over from the "old" node -relocating_deceptive_nodes_generates_a_new_node: True diff --git a/tests/test_configs/config_sections/complete_game_rules_config.yaml b/tests/test_configs/config_sections/complete_game_rules_config.yaml deleted file mode 100644 index e9d5947f..00000000 --- a/tests/test_configs/config_sections/complete_game_rules_config.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# Minimum number of nodes the network this game mode is allowed to run on -min_number_of_network_nodes: 18 -# A lower vulnerability means that a node is less likely to be compromised -node_vulnerability_lower_bound: 0.2 -# A higher vulnerability means that a node is more vulnerable -node_vulnerability_upper_bound: 0.8 -# The max steps that a game can go on for. If the blue agent reaches this they win -max_steps: 1000 -# The blue agent loses if all the nodes become compromised -lose_when_all_nodes_lost: False -# The blue agent loses if n% of the nodes become compromised -lose_when_n_percent_of_nodes_lost: False -# The percentage of nodes that need to be lost for blue to lose -percentage_of_nodes_compromised_equals_loss: 0.8 -# Blue loses if a special 'high value' target is lost (a node picked in the environment) -lose_when_high_value_target_lost: True -# If no high value targets are supplied, how many should be chosen -number_of_high_value_targets: 1 -# The high value target is picked at random -choose_high_value_targets_placement_at_random: False -# The node furthest away from the entry points to the network is picked as the target -choose_high_value_targets_furthest_away_from_entry: True -# If no entry nodes are supplied choose some at random -choose_entry_nodes_randomly: True -# If no entry nodes are supplied then how many should be chosen -number_of_entry_nodes: 3 -# If no entry nodes are supplied then what bias is applied to the nodes when choosing random entry nodes -prefer_central_nodes_for_entry_nodes: True -prefer_edge_nodes_for_entry_nodes: False -# The length of a grace period at the start of the game. During this time the red agent cannot act. This gives the blue agent a chance to "prepare" (A length of 0 means that there is no grace period) -grace_period_length: 0 \ No newline at end of file diff --git a/tests/test_configs/config_sections/complete_observation_space_config.yaml b/tests/test_configs/config_sections/complete_observation_space_config.yaml deleted file mode 100644 index db80d48f..00000000 --- a/tests/test_configs/config_sections/complete_observation_space_config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# The blue agent can see the compromised status of all the nodes -compromised_status: True -# The blue agent can see the vulnerability scores of all the nodes -vulnerabilities: True -# The blue agent can see what nodes are connected to what other nodes -node_connections: True -# The blue agent can see the average vulnerability of all the nodes -average_vulnerability: False -# The blue agent can see a graph connectivity score -graph_connectivity: True -# The blue agent can see all of the nodes that have recently attacked a safe node -attacking_nodes: True -# The blue agent can see all the nodes that have recently been attacked -attacked_nodes: True -# The blue agent can see all of the special nodes (entry nodes, high value targets) -special_nodes: True -# The blue agent can see the skill level of the red agent -red_agent_skill: True \ No newline at end of file diff --git a/tests/test_configs/config_sections/complete_red_agent_config.yaml b/tests/test_configs/config_sections/complete_red_agent_config.yaml deleted file mode 100644 index cbee5fdc..00000000 --- a/tests/test_configs/config_sections/complete_red_agent_config.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# The red agents skill level. Higher means that red is more likely to succeed in attacks -red_skill: 0.5 - -# CHOOSE AT LEAST ONE OF THE FOLLOWING 3 ITEMS (red_ignore_defences: False counts as choosing an item) -# Red uses its skill modifier when attacking nodes -red_uses_skill: True -# The red agent ignores the defences of nodes -red_ignores_defences: False -# Reds attacks always succeed -red_always_succeeds: False - -# The red agent will only ever be in one node however it can control any amount of nodes. Can the red agent only -# attack from its one main node or can it attack from any node that it controls -red_can_only_attack_from_red_agent_node: False -red_can_attack_from_any_red_node: True - -# The red agent naturally spreads its influence every time-step -red_can_naturally_spread: True -# If a node is connected to a compromised node what chance does it have to become compromised every turn through natural spreading -chance_to_spread_to_connected_node: 0.05 -# If a node is not connected to a compromised node what chance does it have to become randomly infected through natural spreading -chance_to_spread_to_unconnected_node: 0 - -# CHOOSE AT LEAST ONE OF THE FOLLOWING 6 ITEMS (EACH ITEM HAS ASSOCIATED WEIGHTING) -# SPREAD: Tries to spread to every node connected to an infected node -red_uses_spread_action: False -# weighting for action -spread_action_likelihood: 1 -# chance for each 'spread' to succeed -chance_for_red_to_spread: 0.1 -# RANDOM INFECT: Tries to infect every safe node in the environment -red_uses_random_infect_action: False -# weighting for action -random_infect_action_likelihood: 1 -# chance for each 'infect' to succeed -chance_for_red_to_random_compromise: 0.1 -# BASIC ATTACK: The red agent picks a single node connected to an infected node and tries to attack and take over that node -red_uses_basic_attack_action: True -# weighting for action -basic_attack_action_likelihood: 2 -# DO NOTHING: The red agent does nothing -red_uses_do_nothing_action: True -do_nothing_action_likelihood: 1 -# The red agent moves to a different node -red_uses_move_action: False -move_action_likelihood: 1 -# ZERO DAY: The red agent will pick a safe node connected to an infect node and take it over with a 100% chance to succeed (can only happen every n timesteps) -red_uses_zero_day_action: True -# The number of zero day attacks that the red agent starts with -zero_day_start_amount: 1 -# The amount of 'progress' that need to have passed before the red agent gains a zero day attack -days_required_for_zero_day: 4 - -# CHOOSE ONE OF THE FOLLOWING 5 ITEMS -# Red picks nodes to attack at random -red_chooses_target_at_random: True -# Red sorts the nodes it can attack and chooses the one that has the most connections -red_prioritises_connected_nodes: False -# Red sorts the nodes it can attack and chooses the one that has the least connections -red_prioritises_un_connected_nodes: False -# Red sorts the nodes is can attack and chooses the one that is the most vulnerable -red_prioritises_vulnerable_nodes: False -# Red sorts the nodes is can attack and chooses the one that is the least vulnerable -red_prioritises_resilient_nodes: False diff --git a/tests/test_configs/config_sections/complete_reset_config.yaml b/tests/test_configs/config_sections/complete_reset_config.yaml deleted file mode 100644 index f70a4281..00000000 --- a/tests/test_configs/config_sections/complete_reset_config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -randomise_vulnerabilities_on_reset: False -choose_new_high_value_targets_on_reset: True -choose_new_entry_nodes_on_reset: True \ No newline at end of file diff --git a/tests/test_configs/config_sections/complete_rewards_config.yaml b/tests/test_configs/config_sections/complete_rewards_config.yaml deleted file mode 100644 index f78ec740..00000000 --- a/tests/test_configs/config_sections/complete_rewards_config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Rewards for the blue agent losing -rewards_for_loss: -100 -# Rewards for the blue agent winning by reaching the maximum number of steps -rewards_for_reaching_max_steps: 100 -# How good the end state is (what % blue controls) is multiplied by the rewards that blue receives for winning -end_rewards_are_multiplied_by_end_state: True -# The negative rewards from the red agent winning are reduced the closer to the end the blue agent gets -reduce_negative_rewards_for_closer_fails: True -# choose the reward method -# There are several built in example reward methods that you can choose from (shown below) -# You can also create your own reward method by copying one of the built in methods and calling it here -# built in reward methods: standard_rewards, one_per_timestep, safe_nodes_give_rewards, punish_bad_actions -reward_function: "standard_rewards" \ No newline at end of file diff --git a/tests/unit_tests/config/agents/__init__.py b/tests/unit_tests/config/agents/__init__.py index 8c2298c2..e69de29b 100644 --- a/tests/unit_tests/config/agents/__init__.py +++ b/tests/unit_tests/config/agents/__init__.py @@ -1,13 +0,0 @@ -import os -from pathlib import Path -from typing import Final - -from tests import TEST_CONFIG_PATH - -TEST_BLUE_AGENT_CONFIG_PATH: Final[Path] = Path( - os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_blue_agent_config.yaml") -) - -TEST_RED_AGENT_CONFIG_PATH: Final[Path] = Path( - os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_red_agent_config.yaml") -) diff --git a/tests/unit_tests/config/agents/test_blue_agent_config.py b/tests/unit_tests/config/agents/test_blue_agent_config.py index 2d4cbc35..34c92cd8 100644 --- a/tests/unit_tests/config/agents/test_blue_agent_config.py +++ b/tests/unit_tests/config/agents/test_blue_agent_config.py @@ -1,15 +1,14 @@ -import os from typing import Dict, Any import pytest -from tests.unit_tests.config.agents import TEST_BLUE_AGENT_CONFIG_PATH -from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig def get_config_dict() -> Dict: - return read_yaml_file(TEST_BLUE_AGENT_CONFIG_PATH) + return read_yaml_file(TEST_BASE_CONFIG_PATH)["BLUE"] def test_read_valid_config(): @@ -19,10 +18,10 @@ def test_read_valid_config(): assert blue_agent.blue_max_deceptive_nodes == 2 # chance_to_immediately_discover_intrusion - assert blue_agent.blue_immediate_detection_chance + assert blue_agent.blue_immediate_detection_chance == 0.5 # chance_to_discover_intrusion_on_scan - assert blue_agent.blue_scan_detection_chance == 1 + assert blue_agent.blue_scan_detection_chance == 0.7 # chance_to_immediately_discover_intrusion_deceptive_node assert blue_agent.blue_deception_immediate_detection_chance == 1 @@ -40,13 +39,13 @@ def test_read_valid_config(): assert blue_agent.blue_discover_attack_source_if_not_detected is True # chance_to_discover_failed_attack - assert blue_agent.blue_chance_to_discover_source_failed == 1 + assert blue_agent.blue_chance_to_discover_source_failed == 0.5 # chance_to_discover_succeeded_attack_compromise_known - assert blue_agent.blue_chance_to_discover_source_succeed_known == 1 + assert blue_agent.blue_chance_to_discover_source_succeed_known == 0.3 # chance_to_discover_succeeded_attack_compromise_not_known - assert blue_agent.blue_chance_to_discover_source_succeed_unknown == 1 + assert blue_agent.blue_chance_to_discover_source_succeed_unknown == 0.1 # chance_to_discover_failed_attack_deceptive_node assert blue_agent.blue_chance_to_discover_source_deceptive_failed == 1 @@ -61,10 +60,10 @@ def test_read_valid_config(): assert blue_agent.blue_vuln_change_amount_make_safe == 0.4 # making_node_safe_gives_random_vulnerability - assert blue_agent.blue_make_safe_random_vuln is False + assert blue_agent.blue_make_safe_random_vuln is True # blue_uses_reduce_vulnerability - assert blue_agent.blue_reduce_vuln_action is False + assert blue_agent.blue_reduce_vuln_action is True # blue_uses_restore_node assert blue_agent.blue_restore_node_action is True @@ -76,16 +75,16 @@ def test_read_valid_config(): assert blue_agent.blue_scan_action is True # blue_uses_isolate_node - assert blue_agent.blue_isolate_action is False + assert blue_agent.blue_isolate_action is True # blue_uses_reconnect_node - assert blue_agent.blue_reconnect_action is False + assert blue_agent.blue_reconnect_action is True # blue_uses_do_nothing is False - assert blue_agent.blue_do_nothing_action is False + assert blue_agent.blue_do_nothing_action is True # blue_uses_deceptive_nodes - assert blue_agent.blue_deceptive_action is False + assert blue_agent.blue_deceptive_action is True # relocating_deceptive_nodes_generates_a_new_node assert blue_agent.blue_deceptive_node_make_new is True diff --git a/tests/unit_tests/config/agents/test_red_agent_config.py b/tests/unit_tests/config/agents/test_red_agent_config.py index 062416f3..56b94396 100644 --- a/tests/unit_tests/config/agents/test_red_agent_config.py +++ b/tests/unit_tests/config/agents/test_red_agent_config.py @@ -2,13 +2,13 @@ import pytest -from tests.unit_tests.config.agents import TEST_RED_AGENT_CONFIG_PATH -from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.agents.red_agent_config import RedAgentConfig def get_config_dict() -> Dict: - return read_yaml_file(TEST_RED_AGENT_CONFIG_PATH) + return read_yaml_file(TEST_BASE_CONFIG_PATH)["RED"] def test_read_valid_config(): @@ -36,10 +36,10 @@ def test_read_valid_config(): assert red_agent.red_naturally_spread is True # chance_to_spread_to_connected_node - assert red_agent.red_chance_to_spread_to_connected_node == 0.05 + assert red_agent.red_chance_to_spread_to_connected_node == 0.01 # chance_to_spread_to_unconnected_node - assert red_agent.red_chance_to_spread_to_unconnected_node == 0 + assert red_agent.red_chance_to_spread_to_unconnected_node == 0.005 # red_uses_spread_action assert red_agent.red_spread_action is False @@ -63,7 +63,7 @@ def test_read_valid_config(): assert red_agent.red_basic_attack_action is True # basic_attack_action_likelihood - assert red_agent.red_basic_attack_likelihood == 2 + assert red_agent.red_basic_attack_likelihood == 1 # red_uses_do_nothing_action assert red_agent.red_do_nothing_action is True @@ -84,13 +84,13 @@ def test_read_valid_config(): assert red_agent.red_zero_day_start_amount == 1 # days_required_for_zero_day - assert red_agent.red_zero_day_days_required_to_create == 4 + assert red_agent.red_zero_day_days_required_to_create == 10 # red_chooses_target_at_random - assert red_agent.red_targeting_random is True + assert red_agent.red_targeting_random is False # red_prioritises_connected_nodes - assert red_agent.red_targeting_prioritise_connected_nodes is False + assert red_agent.red_targeting_prioritise_connected_nodes is True # red_prioritises_un_connected_nodes assert red_agent.red_targeting_prioritise_unconnected_nodes is False diff --git a/tests/unit_tests/config/environment/__init__.py b/tests/unit_tests/config/environment/__init__.py index 71585c3b..e69de29b 100644 --- a/tests/unit_tests/config/environment/__init__.py +++ b/tests/unit_tests/config/environment/__init__.py @@ -1,22 +0,0 @@ -from typing import Final -import os -from pathlib import Path -from typing import Final - -from tests import TEST_CONFIG_PATH - -TEST_GAME_RULES_CONFIG_PATH: Final[Path] = Path( - os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_game_rules_config.yaml") -) - -TEST_OBSERVATION_SPACE_CONFIG_PATH: Final[Path] = Path( - os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_observation_space_config.yaml") -) - -TEST_RESET_CONFIG_PATH: Final[Path] = Path( - os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_reset_config.yaml") -) - -TEST_REWARDS_CONFIG_PATH: Final[Path] = Path( - os.path.join(TEST_CONFIG_PATH, "config_sections", "complete_rewards_config.yaml") -) diff --git a/tests/unit_tests/config/environment/test_game_rules_config.py b/tests/unit_tests/config/environment/test_game_rules_config.py index 775f3572..bd6482ca 100644 --- a/tests/unit_tests/config/environment/test_game_rules_config.py +++ b/tests/unit_tests/config/environment/test_game_rules_config.py @@ -2,13 +2,13 @@ import pytest -from tests.unit_tests.config.config_test_utils import read_yaml_file -from tests.unit_tests.config.environment import TEST_GAME_RULES_CONFIG_PATH +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.environment.game_rules_config import GameRulesConfig def get_config_dict() -> Dict: - return read_yaml_file(TEST_GAME_RULES_CONFIG_PATH) + return read_yaml_file(TEST_BASE_CONFIG_PATH)["GAME_RULES"] def test_read_valid_config(): @@ -24,13 +24,13 @@ def test_read_valid_config(): assert game_rules.gr_loss_total_compromise is False - assert game_rules.gr_loss_pc_nodes_compromised is False + assert game_rules.gr_loss_pc_nodes_compromised is True assert game_rules.gr_loss_pc_node_compromised_pc == 0.8 assert game_rules.gr_number_of_high_value_targets == 1 - assert game_rules.gr_loss_hvt is True + assert game_rules.gr_loss_hvt is False assert game_rules.gr_loss_hvt_random_placement is False @@ -40,11 +40,11 @@ def test_read_valid_config(): assert game_rules.gr_num_entry_nodes == 3 - assert game_rules.gr_prefer_central_entry is True + assert game_rules.gr_prefer_central_entry is False assert game_rules.gr_prefer_edge_nodes is False - assert game_rules.gr_grace_period == 0 + assert game_rules.gr_grace_period == 3 @pytest.mark.parametrize( diff --git a/tests/unit_tests/config/environment/test_observation_space_config.py b/tests/unit_tests/config/environment/test_observation_space_config.py index bc8c2643..d5dfe4c7 100644 --- a/tests/unit_tests/config/environment/test_observation_space_config.py +++ b/tests/unit_tests/config/environment/test_observation_space_config.py @@ -2,13 +2,13 @@ import pytest -from tests.unit_tests.config.config_test_utils import read_yaml_file -from tests.unit_tests.config.environment import TEST_OBSERVATION_SPACE_CONFIG_PATH +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.environment.observation_space_config import ObservationSpaceConfig def get_config_dict() -> Dict: - return read_yaml_file(TEST_OBSERVATION_SPACE_CONFIG_PATH) + return read_yaml_file(TEST_BASE_CONFIG_PATH)["OBSERVATION_SPACE"] def test_read_valid_config(): @@ -28,7 +28,7 @@ def test_read_valid_config(): assert obs_space.obs_attack_targets is True - assert obs_space.obs_special_nodes is True + assert obs_space.obs_special_nodes is False assert obs_space.obs_red_agent_skill is True diff --git a/tests/unit_tests/config/environment/test_reset_config.py b/tests/unit_tests/config/environment/test_reset_config.py index b2c757de..a495da70 100644 --- a/tests/unit_tests/config/environment/test_reset_config.py +++ b/tests/unit_tests/config/environment/test_reset_config.py @@ -2,13 +2,13 @@ import pytest -from tests.unit_tests.config.config_test_utils import read_yaml_file -from tests.unit_tests.config.environment import TEST_RESET_CONFIG_PATH +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.environment.reset_config import ResetConfig def get_config_dict() -> Dict: - return read_yaml_file(TEST_RESET_CONFIG_PATH) + return read_yaml_file(TEST_BASE_CONFIG_PATH)["RESET"] def test_read_valid_config(): @@ -16,9 +16,9 @@ def test_read_valid_config(): assert reset_config.reset_random_vulns is False - assert reset_config.reset_move_hvt is True + assert reset_config.reset_move_hvt is False - assert reset_config.reset_move_entry_nodes is True + assert reset_config.reset_move_entry_nodes is False @pytest.mark.parametrize( diff --git a/tests/unit_tests/config/environment/test_rewards_config.py b/tests/unit_tests/config/environment/test_rewards_config.py index f91f33c6..06dc2789 100644 --- a/tests/unit_tests/config/environment/test_rewards_config.py +++ b/tests/unit_tests/config/environment/test_rewards_config.py @@ -2,13 +2,13 @@ import pytest -from tests.unit_tests.config.config_test_utils import read_yaml_file -from tests.unit_tests.config.environment import TEST_REWARDS_CONFIG_PATH +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.environment.rewards_config import RewardsConfig def get_config_dict() -> Dict: - return read_yaml_file(TEST_REWARDS_CONFIG_PATH) + return read_yaml_file(TEST_BASE_CONFIG_PATH)["REWARDS"] def test_read_valid_config(): @@ -20,7 +20,7 @@ def test_read_valid_config(): assert rewards_config.reward_end_multiplier is True - assert rewards_config.reward_reduce_negative_rewards is True + assert rewards_config.reward_reduce_negative_rewards is False assert rewards_config.reward_function == "standard_rewards" diff --git a/tests/unit_tests/config/game_config/test_game_mode_config.py b/tests/unit_tests/config/game_config/test_game_mode_config.py index ff037dda..cbb24fb2 100644 --- a/tests/unit_tests/config/game_config/test_game_mode_config.py +++ b/tests/unit_tests/config/game_config/test_game_mode_config.py @@ -4,8 +4,8 @@ import pytest -from tests import TEST_CONFIG_PATH -from tests.unit_tests.config.config_test_utils import read_yaml_file +from tests import TEST_BASE_CONFIG_PATH +from tests.config_test_utils import read_yaml_file from yawning_titan.config.agents.blue_agent_config import BlueAgentConfig from yawning_titan.config.agents.red_agent_config import RedAgentConfig from yawning_titan.config.environment.game_rules_config import GameRulesConfig @@ -17,7 +17,7 @@ def get_config_dict() -> Dict: - return read_yaml_file(Path(os.path.join(TEST_CONFIG_PATH, "base_config.yaml"))) + return read_yaml_file(TEST_BASE_CONFIG_PATH) def get_default_config_dict() -> Dict: @@ -25,7 +25,7 @@ def get_default_config_dict() -> Dict: def test_read_valid_path_and_valid_config(): - game_mode = GameModeConfig.create(os.path.join(TEST_CONFIG_PATH, "base_config.yaml")) + game_mode = GameModeConfig.create(TEST_BASE_CONFIG_PATH) assert game_mode.red_agent_config == RedAgentConfig.create(get_config_dict()["RED"]) assert game_mode.blue_agent_config == BlueAgentConfig.create(get_config_dict()["BLUE"]) diff --git a/tests/unit_tests/config/network_config/test_network_config.py b/tests/unit_tests/config/network_config/test_network_config.py index 90310c71..0c42555f 100644 --- a/tests/unit_tests/config/network_config/test_network_config.py +++ b/tests/unit_tests/config/network_config/test_network_config.py @@ -24,7 +24,7 @@ def test_config_properties(): def test_hvn_entry_node_matching(): with warnings.catch_warnings(record=True) as w: - network_config = NetworkConfig.create( + NetworkConfig.create( matrix=matrix, positions=node_positions, entry_nodes=["0"], diff --git a/yawning_titan/config/game_config/game_mode_config.py b/yawning_titan/config/game_config/game_mode_config.py index e5aac90c..8f169688 100644 --- a/yawning_titan/config/game_config/game_mode_config.py +++ b/yawning_titan/config/game_config/game_mode_config.py @@ -78,7 +78,6 @@ def create( settings = yaml.load(f, Loader=SafeLoader) except FileNotFoundError as e: msg = f"Configuration file does not exist: {settings_path}" - print(msg) # TODO: Remove once proper logging is setup _LOGGER.critical(msg, exc_info=True) raise e