901 - merged dev into my branch
This commit is contained in:
@@ -355,21 +355,50 @@ class AccessControlList(AbstractObservationComponent):
|
||||
# 3. Initialise observation with zeroes
|
||||
self.current_observation = np.zeros(len(shape), dtype=self._DATA_TYPE)
|
||||
|
||||
# Dictionary to map services to numbers for obs space
|
||||
self.services_dict = {}
|
||||
|
||||
def update(self):
|
||||
"""Update the observation based on current environment state.
|
||||
|
||||
The structure of the observation space is described in :class:`.AccessControlList`
|
||||
"""
|
||||
obs = []
|
||||
for acl_rule in self.env.acl:
|
||||
for acl_rule in self.env.acl.acl:
|
||||
permission = acl_rule.permission
|
||||
source_ip = acl_rule.source_ip
|
||||
dest_ip = acl_rule.dest_ip
|
||||
protocol = acl_rule.protocol
|
||||
port = acl_rule.port
|
||||
position = self.env.acl.index(acl_rule)
|
||||
position = self.env.acl.acl.index(acl_rule)
|
||||
if permission == "DENY":
|
||||
permission_int = 0
|
||||
else:
|
||||
permission_int = 1
|
||||
|
||||
obs.extend([permission, source_ip, dest_ip, protocol, port, position])
|
||||
if source_ip == "ANY":
|
||||
source_ip = 0
|
||||
if dest_ip == "ANY":
|
||||
dest_ip = 0
|
||||
if port == "ANY":
|
||||
port = 0
|
||||
if protocol == "ANY":
|
||||
protocol_int = 0
|
||||
else:
|
||||
while True:
|
||||
if protocol in self.service_dict:
|
||||
protocol_int = self.services_dict[protocol]
|
||||
break
|
||||
else:
|
||||
self.services_dict[protocol] = len(self.services_dict) + 1
|
||||
continue
|
||||
# [0 - DENY, 1 - ALLOW] Permission
|
||||
# [0 - ANY, x - IP Address/Protocol/Port]
|
||||
|
||||
print(permission_int, source_ip, dest_ip, protocol_int, port)
|
||||
obs.extend(
|
||||
[permission_int, source_ip, dest_ip, protocol_int, port, position]
|
||||
)
|
||||
|
||||
self.current_observation[:] = obs
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Crown Copyright (C) Dstl 2022. DEFCON 703. Shared in confidence.
|
||||
"""Main environment module containing the PRIMmary AI Training Evironment (Primaite) class."""
|
||||
|
||||
import copy
|
||||
import csv
|
||||
import logging
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple, Union
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
@@ -24,11 +23,13 @@ from primaite.common.enums import (
|
||||
NodePOLInitiator,
|
||||
NodePOLType,
|
||||
NodeType,
|
||||
ObservationType,
|
||||
Priority,
|
||||
RulePermissionType,
|
||||
SoftwareState,
|
||||
)
|
||||
from primaite.common.service import Service
|
||||
from primaite.config import training_config
|
||||
from primaite.config.training_config import TrainingConfig
|
||||
from primaite.environment.observations import ObservationsHandler
|
||||
from primaite.environment.reward import calculate_reward_function
|
||||
from primaite.links.link import Link
|
||||
@@ -56,29 +57,41 @@ class Primaite(Env):
|
||||
ACTION_SPACE_ACL_ACTION_VALUES: int = 3
|
||||
ACTION_SPACE_ACL_PERMISSION_VALUES: int = 2
|
||||
|
||||
def __init__(self, _config_values, _transaction_list):
|
||||
def __init__(
|
||||
self,
|
||||
training_config_path: Union[str, Path],
|
||||
lay_down_config_path: Union[str, Path],
|
||||
transaction_list,
|
||||
session_path: Path,
|
||||
timestamp_str: str,
|
||||
):
|
||||
"""
|
||||
Init.
|
||||
The Primaite constructor.
|
||||
|
||||
Args:
|
||||
_episode_steps: The number of steps for the episode
|
||||
_config_filename: The name of config file
|
||||
_transaction_list: The list of transactions to populate
|
||||
_agent_identifier: Identifier for the agent
|
||||
:param training_config_path: The training config filepath.
|
||||
:param lay_down_config_path: The lay down config filepath.
|
||||
:param transaction_list: The list of transactions to populate.
|
||||
:param session_path: The directory path the session is writing to.
|
||||
:param timestamp_str: The session timestamp in the format:
|
||||
<yyyy-mm-dd>_<hh-mm-ss>.
|
||||
"""
|
||||
super(Primaite, self).__init__()
|
||||
self._training_config_path = training_config_path
|
||||
self._lay_down_config_path = lay_down_config_path
|
||||
|
||||
# Take a copy of the config values
|
||||
self.config_values = _config_values
|
||||
self.training_config: TrainingConfig = training_config.load(
|
||||
training_config_path
|
||||
)
|
||||
|
||||
# Number of steps in an episode
|
||||
self.episode_steps = 0
|
||||
self.episode_steps = self.training_config.num_steps
|
||||
|
||||
super(Primaite, self).__init__()
|
||||
|
||||
# Transaction list
|
||||
self.transaction_list = _transaction_list
|
||||
self.transaction_list = transaction_list
|
||||
|
||||
# The agent in use
|
||||
self.agent_identifier = self.config_values.agent_identifier
|
||||
self.agent_identifier = self.training_config.agent_identifier
|
||||
|
||||
# Create a dictionary to hold all the nodes
|
||||
self.nodes: Dict[str, NodeUnion] = {}
|
||||
@@ -122,6 +135,9 @@ class Primaite(Env):
|
||||
# Create step count
|
||||
self.step_count = 0
|
||||
|
||||
self.total_step_count: int = 0
|
||||
"""The total number of time steps completed."""
|
||||
|
||||
# Create step info dictionary
|
||||
self.step_info = {}
|
||||
|
||||
@@ -149,29 +165,21 @@ class Primaite(Env):
|
||||
# The action type
|
||||
self.action_type = 0
|
||||
|
||||
# TODO fix up with TrainingConfig
|
||||
# stores the observation config from the yaml, default is NODE_LINK_TABLE
|
||||
self.obs_config: dict = {"components": [{"name": "NODE_LINK_TABLE"}]}
|
||||
if self.config_values.observation_config is not None:
|
||||
self.obs_config = self.config_values.observation_config
|
||||
if self.training_config.observation_space is not None:
|
||||
self.obs_config = self.training_config.observation_space
|
||||
|
||||
# Observation Handler manages the user-configurable observation space.
|
||||
# It will be initialised later.
|
||||
self.obs_handler: ObservationsHandler
|
||||
|
||||
# Set by main_config
|
||||
# Adds a DENY ALL or ALLOW ALL to the end of the Access Control List
|
||||
self.acl_implicit_rule = RulePermissionType.DENY
|
||||
|
||||
# Sets a limit to how many ACL
|
||||
self.max_acl_rules = 0
|
||||
# Open the config file and build the environment laydown
|
||||
try:
|
||||
self.config_file = open(self.config_values.config_filename_use_case, "r")
|
||||
self.config_data = yaml.safe_load(self.config_file)
|
||||
self.load_config()
|
||||
except Exception:
|
||||
_LOGGER.error("Could not load the environment configuration")
|
||||
_LOGGER.error("Exception occured", exc_info=True)
|
||||
with open(self._lay_down_config_path, "r") as file:
|
||||
# Open the config file and build the environment laydown
|
||||
self.lay_down_config = yaml.safe_load(file)
|
||||
self.load_lay_down_config()
|
||||
|
||||
# Store the node objects as node attributes
|
||||
# (This is so we can access them as objects)
|
||||
@@ -188,15 +196,10 @@ class Primaite(Env):
|
||||
try:
|
||||
plt.tight_layout()
|
||||
nx.draw_networkx(self.network, with_labels=True)
|
||||
now = datetime.now() # current date and time
|
||||
time = now.strftime("%Y%m%d_%H%M%S")
|
||||
datetime.now() # current date and time
|
||||
|
||||
path = "outputs/diagrams"
|
||||
is_dir = os.path.isdir(path)
|
||||
if not is_dir:
|
||||
os.makedirs(path)
|
||||
filename = "outputs/diagrams/network_" + time + ".png"
|
||||
plt.savefig(filename, format="PNG")
|
||||
file_path = session_path / f"network_{timestamp_str}.png"
|
||||
plt.savefig(file_path, format="PNG")
|
||||
plt.clf()
|
||||
except Exception:
|
||||
_LOGGER.error("Could not save network diagram")
|
||||
@@ -207,7 +210,7 @@ class Primaite(Env):
|
||||
self.observation_space, self.env_obs = self.init_observations()
|
||||
|
||||
# Define Action Space - depends on action space type (Node or ACL)
|
||||
if self.action_type == ActionType.NODE:
|
||||
if self.training_config.action_type == ActionType.NODE:
|
||||
_LOGGER.info("Action space type NODE selected")
|
||||
# Terms (for node action space):
|
||||
# [0, num nodes] - node ID (0 = nothing, node ID)
|
||||
@@ -216,7 +219,7 @@ class Primaite(Env):
|
||||
# [0, num services] - resolves to service ID (0 = nothing, resolves to service) # noqa
|
||||
self.action_dict = self.create_node_action_dict()
|
||||
self.action_space = spaces.Discrete(len(self.action_dict))
|
||||
elif self.action_type == ActionType.ACL:
|
||||
elif self.training_config.action_type == ActionType.ACL:
|
||||
_LOGGER.info("Action space type ACL selected")
|
||||
# Terms (for ACL action space):
|
||||
# [0, 2] - Action (0 = do nothing, 1 = create rule, 2 = delete rule)
|
||||
@@ -227,25 +230,21 @@ class Primaite(Env):
|
||||
# [0, num ports] - Port (0 = any, then 1 -> x resolving to port)
|
||||
self.action_dict = self.create_acl_action_dict()
|
||||
self.action_space = spaces.Discrete(len(self.action_dict))
|
||||
elif self.action_type == ActionType.ANY:
|
||||
elif self.training_config.action_type == ActionType.ANY:
|
||||
_LOGGER.info("Action space type ANY selected - Node + ACL")
|
||||
self.action_dict = self.create_node_and_acl_action_dict()
|
||||
self.action_space = spaces.Discrete(len(self.action_dict))
|
||||
else:
|
||||
_LOGGER.info("Invalid action type selected")
|
||||
_LOGGER.info(
|
||||
f"Invalid action type selected: {self.training_config.action_type}"
|
||||
)
|
||||
# Set up a csv to store the results of the training
|
||||
try:
|
||||
now = datetime.now() # current date and time
|
||||
time = now.strftime("%Y%m%d_%H%M%S")
|
||||
header = ["Episode", "Average Reward"]
|
||||
|
||||
# Check whether the output/rerults folder exists (doesn't exist by default install)
|
||||
path = "outputs/results/"
|
||||
is_dir = os.path.isdir(path)
|
||||
if not is_dir:
|
||||
os.makedirs(path)
|
||||
filename = "outputs/results/average_reward_per_episode_" + time + ".csv"
|
||||
self.csv_file = open(filename, "w", encoding="UTF8", newline="")
|
||||
file_name = f"average_reward_per_episode_{timestamp_str}.csv"
|
||||
file_path = session_path / file_name
|
||||
self.csv_file = open(file_path, "w", encoding="UTF8", newline="")
|
||||
self.csv_writer = csv.writer(self.csv_file)
|
||||
self.csv_writer.writerow(header)
|
||||
except Exception:
|
||||
@@ -304,7 +303,8 @@ class Primaite(Env):
|
||||
done = False
|
||||
|
||||
self.step_count += 1
|
||||
# print("Episode step: " + str(self.stepCount))
|
||||
self.total_step_count += 1
|
||||
# print("Episode step: " + str(self.step_count))
|
||||
|
||||
# Need to clear traffic on all links first
|
||||
for link_key, link_value in self.links.items():
|
||||
@@ -376,13 +376,13 @@ class Primaite(Env):
|
||||
self.green_iers,
|
||||
self.red_iers,
|
||||
self.step_count,
|
||||
self.config_values,
|
||||
self.training_config,
|
||||
)
|
||||
print(f" Step {self.step_count} Reward: {str(reward)}")
|
||||
# print(f" Step {self.step_count} Reward: {str(reward)}")
|
||||
self.total_reward += reward
|
||||
if self.step_count == self.episode_steps:
|
||||
self.average_reward = self.total_reward / self.step_count
|
||||
if self.config_values.session_type == "EVALUATION":
|
||||
if self.training_config.session_type == "EVALUATION":
|
||||
# For evaluation, need to trigger the done value = True when
|
||||
# step count is reached in order to prevent neverending episode
|
||||
done = True
|
||||
@@ -407,7 +407,6 @@ class Primaite(Env):
|
||||
def __close__(self):
|
||||
"""Override close function."""
|
||||
self.csv_file.close()
|
||||
self.config_file.close()
|
||||
|
||||
def init_acl(self):
|
||||
"""Initialise the Access Control List."""
|
||||
@@ -433,9 +432,10 @@ class Primaite(Env):
|
||||
_action: The action space from the agent
|
||||
"""
|
||||
# At the moment, actions are only affecting nodes
|
||||
if self.action_type == ActionType.NODE:
|
||||
|
||||
if self.training_config.action_type == ActionType.NODE:
|
||||
self.apply_actions_to_nodes(_action)
|
||||
elif self.action_type == ActionType.ACL:
|
||||
elif self.training_config.action_type == ActionType.ACL:
|
||||
self.apply_actions_to_acl(_action)
|
||||
elif (
|
||||
len(self.action_dict[_action]) == 6
|
||||
@@ -673,44 +673,39 @@ class Primaite(Env):
|
||||
self.obs_handler.update_obs()
|
||||
self.env_obs = self.obs_handler.current_observation
|
||||
|
||||
def load_config(self):
|
||||
def load_lay_down_config(self):
|
||||
"""Loads config data in order to build the environment configuration."""
|
||||
for item in self.config_data:
|
||||
if item["itemType"] == "NODE":
|
||||
for item in self.lay_down_config:
|
||||
if item["item_type"] == "NODE":
|
||||
# Create a node
|
||||
self.create_node(item)
|
||||
elif item["itemType"] == "LINK":
|
||||
elif item["item_type"] == "LINK":
|
||||
# Create a link
|
||||
self.create_link(item)
|
||||
elif item["itemType"] == "GREEN_IER":
|
||||
elif item["item_type"] == "GREEN_IER":
|
||||
# Create a Green IER
|
||||
self.create_green_ier(item)
|
||||
elif item["itemType"] == "GREEN_POL":
|
||||
elif item["item_type"] == "GREEN_POL":
|
||||
# Create a Green PoL
|
||||
self.create_green_pol(item)
|
||||
elif item["itemType"] == "RED_IER":
|
||||
elif item["item_type"] == "RED_IER":
|
||||
# Create a Red IER
|
||||
self.create_red_ier(item)
|
||||
elif item["itemType"] == "RED_POL":
|
||||
elif item["item_type"] == "RED_POL":
|
||||
# Create a Red PoL
|
||||
self.create_red_pol(item)
|
||||
elif item["itemType"] == "ACL_RULE":
|
||||
elif item["item_type"] == "ACL_RULE":
|
||||
# Create an ACL rule
|
||||
self.create_acl_rule(item)
|
||||
elif item["itemType"] == "SERVICES":
|
||||
elif item["item_type"] == "SERVICES":
|
||||
# Create the list of services
|
||||
self.create_services_list(item)
|
||||
elif item["itemType"] == "PORTS":
|
||||
elif item["item_type"] == "PORTS":
|
||||
# Create the list of ports
|
||||
self.create_ports_list(item)
|
||||
elif item["itemType"] == "ACTIONS":
|
||||
# Get the action information
|
||||
self.get_action_info(item)
|
||||
elif item["itemType"] == "STEPS":
|
||||
# Get the steps information
|
||||
self.get_steps_info(item)
|
||||
else:
|
||||
# Do nothing (bad formatting)
|
||||
item_type = item["item_type"]
|
||||
_LOGGER.error(f"Invalid item_type: {item_type}")
|
||||
pass
|
||||
|
||||
_LOGGER.info("Environment configuration loaded")
|
||||
@@ -738,7 +733,7 @@ class Primaite(Env):
|
||||
node_type,
|
||||
node_priority,
|
||||
node_hardware_state,
|
||||
self.config_values,
|
||||
self.training_config,
|
||||
)
|
||||
elif node_class == "ACTIVE":
|
||||
# Active nodes have IP address, Software State and file system state
|
||||
@@ -754,7 +749,7 @@ class Primaite(Env):
|
||||
node_ip_address,
|
||||
node_software_state,
|
||||
node_file_system_state,
|
||||
self.config_values,
|
||||
self.training_config,
|
||||
)
|
||||
elif node_class == "SERVICE":
|
||||
# Service nodes have IP address, Software State, file system state and list of services
|
||||
@@ -770,7 +765,7 @@ class Primaite(Env):
|
||||
node_ip_address,
|
||||
node_software_state,
|
||||
node_file_system_state,
|
||||
self.config_values,
|
||||
self.training_config,
|
||||
)
|
||||
node_services = item["services"]
|
||||
for service in node_services:
|
||||
@@ -849,14 +844,14 @@ class Primaite(Env):
|
||||
item: A config data item
|
||||
"""
|
||||
ier_id = item["id"]
|
||||
ier_start_step = item["startStep"]
|
||||
ier_end_step = item["endStep"]
|
||||
ier_start_step = item["start_step"]
|
||||
ier_end_step = item["end_step"]
|
||||
ier_load = item["load"]
|
||||
ier_protocol = item["protocol"]
|
||||
ier_port = item["port"]
|
||||
ier_source = item["source"]
|
||||
ier_destination = item["destination"]
|
||||
ier_mission_criticality = item["missionCriticality"]
|
||||
ier_mission_criticality = item["mission_criticality"]
|
||||
|
||||
# Create IER and add to green IER dictionary
|
||||
self.green_iers[ier_id] = IER(
|
||||
@@ -879,14 +874,14 @@ class Primaite(Env):
|
||||
item: A config data item
|
||||
"""
|
||||
ier_id = item["id"]
|
||||
ier_start_step = item["startStep"]
|
||||
ier_end_step = item["endStep"]
|
||||
ier_start_step = item["start_step"]
|
||||
ier_end_step = item["end_step"]
|
||||
ier_load = item["load"]
|
||||
ier_protocol = item["protocol"]
|
||||
ier_port = item["port"]
|
||||
ier_source = item["source"]
|
||||
ier_destination = item["destination"]
|
||||
ier_mission_criticality = item["missionCriticality"]
|
||||
ier_mission_criticality = item["mission_criticality"]
|
||||
|
||||
# Create IER and add to red IER dictionary
|
||||
self.red_iers[ier_id] = IER(
|
||||
@@ -909,8 +904,8 @@ class Primaite(Env):
|
||||
item: A config data item
|
||||
"""
|
||||
pol_id = item["id"]
|
||||
pol_start_step = item["startStep"]
|
||||
pol_end_step = item["endStep"]
|
||||
pol_start_step = item["start_step"]
|
||||
pol_end_step = item["end_step"]
|
||||
pol_node = item["nodeId"]
|
||||
pol_type = NodePOLType[item["type"]]
|
||||
|
||||
@@ -943,8 +938,8 @@ class Primaite(Env):
|
||||
item: A config data item
|
||||
"""
|
||||
pol_id = item["id"]
|
||||
pol_start_step = item["startStep"]
|
||||
pol_end_step = item["endStep"]
|
||||
pol_start_step = item["start_step"]
|
||||
pol_end_step = item["end_step"]
|
||||
pol_target_node_id = item["targetNodeId"]
|
||||
pol_initiator = NodePOLInitiator[item["initiator"]]
|
||||
pol_type = NodePOLType[item["type"]]
|
||||
@@ -1004,7 +999,7 @@ class Primaite(Env):
|
||||
Args:
|
||||
item: A config data item representing the services
|
||||
"""
|
||||
service_list = services["serviceList"]
|
||||
service_list = services["service_list"]
|
||||
|
||||
for service in service_list:
|
||||
service_name = service["name"]
|
||||
@@ -1020,7 +1015,7 @@ class Primaite(Env):
|
||||
Args:
|
||||
item: A config data item representing the ports
|
||||
"""
|
||||
ports_list = ports["portsList"]
|
||||
ports_list = ports["ports_list"]
|
||||
|
||||
for port in ports_list:
|
||||
port_value = port["port"]
|
||||
@@ -1029,6 +1024,14 @@ class Primaite(Env):
|
||||
# Set the number of ports
|
||||
self.num_ports = len(self.ports_list)
|
||||
|
||||
def get_observation_info(self, observation_info):
|
||||
"""Extracts observation_info.
|
||||
|
||||
:param observation_info: Config item that defines which type of observation space to use
|
||||
:type observation_info: str
|
||||
"""
|
||||
self.observation_type = ObservationType[observation_info["type"]]
|
||||
|
||||
def get_action_info(self, action_info):
|
||||
"""
|
||||
Extracts action_info.
|
||||
@@ -1050,16 +1053,6 @@ class Primaite(Env):
|
||||
"""
|
||||
self.obs_config = obs_config
|
||||
|
||||
def get_steps_info(self, steps_info):
|
||||
"""
|
||||
Extracts steps_info.
|
||||
|
||||
Args:
|
||||
item: A config data item representing steps info
|
||||
"""
|
||||
self.episode_steps = int(steps_info["steps"])
|
||||
_LOGGER.info("Training episodes have " + str(self.episode_steps) + " steps")
|
||||
|
||||
def reset_environment(self):
|
||||
"""
|
||||
# Resets environment.
|
||||
@@ -1067,11 +1060,11 @@ class Primaite(Env):
|
||||
Uses config data config data in order to build the environment
|
||||
configuration.
|
||||
"""
|
||||
for item in self.config_data:
|
||||
if item["itemType"] == "NODE":
|
||||
for item in self.lay_down_config:
|
||||
if item["item_type"] == "NODE":
|
||||
# Reset a node's state (normal and reference)
|
||||
self.reset_node(item)
|
||||
elif item["itemType"] == "ACL_RULE":
|
||||
elif item["item_type"] == "ACL_RULE":
|
||||
# Create an ACL rule (these are cleared on reset, so just need to recreate them)
|
||||
self.create_acl_rule(item)
|
||||
else:
|
||||
@@ -1173,7 +1166,7 @@ class Primaite(Env):
|
||||
def create_acl_action_dict(self):
|
||||
"""Creates a dictionary mapping each possible discrete action to more readable multidiscrete action."""
|
||||
# reserve 0 action to be a nothing action
|
||||
actions = {0: [0, 0, 0, 0, 0, 0, 0]}
|
||||
actions = {0: [0, 0, 0, 0, 0, 0]}
|
||||
|
||||
action_key = 1
|
||||
# 3 possible action decisions, 0=NOTHING, 1=CREATE, 2=DELETE
|
||||
@@ -1185,16 +1178,14 @@ class Primaite(Env):
|
||||
for dest_ip in range(self.num_nodes + 1):
|
||||
for protocol in range(self.num_services + 1):
|
||||
for port in range(self.num_ports + 1):
|
||||
for position in range(self.max_acl_rules - 1):
|
||||
action = [
|
||||
action_decision,
|
||||
action_permission,
|
||||
source_ip,
|
||||
dest_ip,
|
||||
protocol,
|
||||
port,
|
||||
position,
|
||||
]
|
||||
action = [
|
||||
action_decision,
|
||||
action_permission,
|
||||
source_ip,
|
||||
dest_ip,
|
||||
protocol,
|
||||
port,
|
||||
]
|
||||
# Check to see if its an action we want to include as possible i.e. not a nothing action
|
||||
if is_valid_acl_action_extra(action):
|
||||
actions[action_key] = action
|
||||
|
||||
Reference in New Issue
Block a user