def test_get_node_pos(self): num_rows = 3 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) assert network_config.get_node_pos(0) == (0, 1) assert network_config.get_node_pos(1) == (1, 0) assert network_config.get_node_pos(2) == (1, 1) assert network_config.get_node_pos(3) == (1, 2) assert network_config.get_node_pos(4) == (2, 1)
def test_get_coords_of_adjacency_matrix_id(self): num_rows = 2 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) assert network_config.get_coords_of_adjacency_matrix_id(0) == (0, 0) assert network_config.get_coords_of_adjacency_matrix_id(1) == (0, 1) assert network_config.get_coords_of_adjacency_matrix_id(2) == (0, 2) assert network_config.get_coords_of_adjacency_matrix_id(3) == (1, 0) assert network_config.get_coords_of_adjacency_matrix_id(4) == (1, 1) assert network_config.get_coords_of_adjacency_matrix_id(5) == (1, 2)
def randomize_attacker_position(self, network_config: NetworkConfig): temp_rows = list(range(1, network_config.num_rows)) temp_cols = list(range(0, network_config.num_cols)) positions = [] for r in temp_rows: for c in temp_cols: node_id = network_config.get_node_id((r, c)) if network_config.node_list[node_id] == NodeType.SERVER.value \ or network_config.node_list[node_id] == NodeType.START.value: positions.append((r, c)) rnd_idx = np.random.choice(list(range(len(positions)))) rnd_pos = positions[rnd_idx] id = network_config.get_node_id(rnd_pos) if network_config.node_list[id] == NodeType.START.value: rnd_pos = network_config.start_pos self.attacker_pos = rnd_pos
def test_node_list(self): num_rows = 3 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) node_list = network_config.node_list assert len(node_list) == num_rows*num_cols - 4 for i in range(len(node_list)): assert node_list[i] is not None
def test_initialization(self): num_rows = 2 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) assert network_config.graph_layout.shape == (num_rows, num_cols) for i in range(network_config.num_rows): for j in range(network_config.num_cols): assert network_config.graph_layout[i][j] is not None assert network_config.adjacency_matrix.shape == (num_rows*num_cols, num_rows*num_cols) for i in range(network_config.num_rows*network_config.num_cols): for j in range(network_config.num_cols*network_config.num_rows): assert network_config.adjacency_matrix[i][j] == 1 or network_config.adjacency_matrix[i][j] == 0
def test_copy(self): state = GameState() num_attack_types = 10 rows = 4 cols = 4 network_config = NetworkConfig(rows, cols) num_nodes = len(network_config.node_list) state.default_state(list(range(num_nodes)), (3, 1), num_attack_types, network_config) copy = state.copy() assert copy.num_hacks == state.num_hacks assert np.array_equal(copy.attack_values, state.attack_values) assert np.array_equal(copy.defense_det, state.defense_det) assert np.array_equal(copy.defense_values, state.defense_values)
def test_get_node_id(self): num_rows = 3 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) assert network_config.get_node_id((0,1)) == 0 assert network_config.get_node_id((1, 0)) == 1 assert network_config.get_node_id((1, 1)) == 2 assert network_config.get_node_id((1, 2)) == 3 assert network_config.get_node_id((2, 1)) == 4 assert network_config.get_node_id((2, 2)) == -1 assert network_config.get_node_id((2, 0)) == -1 assert network_config.get_node_id((0, 0)) == -1 assert network_config.get_node_id((0, 2)) == -1
def test_default_state(self): state = GameState() rows = 4 cols = 4 network_config = NetworkConfig(rows, cols) num_nodes = len(network_config.node_list) num_attack_types = 10 state.default_state(list(range(num_nodes)), (3, 1), num_attack_types, network_config) assert state.attack_values.shape == (num_nodes, 10) assert state.defense_values.shape == (num_nodes, 10) assert state.defense_det.shape == (num_nodes, ) assert state.attacker_pos == (3, 1) assert state.done == False assert state.hacked == False assert state.num_hacks == 0 assert state.detected == False assert state.num_games == 0
def test_simulate_attack(self): rows = 4 cols = 4 network_config = NetworkConfig(rows, cols) num_nodes = len(network_config.node_list) state = GameState() num_attack_types = 10 state.default_state(list(range(num_nodes)), (3, 1), num_attack_types, network_config) attack_node_id = 3 attack_type = 4 state.defense_values[attack_node_id][attack_type] = 5 state.attack_values[attack_node_id][attack_type] = 5 assert not state.simulate_attack(attack_node_id, attack_type, network_config) state.defense_values[attack_node_id][attack_type] = 5 state.attack_values[attack_node_id][attack_type] = 6 assert state.simulate_attack(attack_node_id, attack_type, network_config)
def test_defend(self): state = GameState() rows = 4 cols = 4 network_config = NetworkConfig(rows, cols) num_nodes = len(network_config.node_list) num_attack_types = 10 state.default_state(list(range(num_nodes)), (3, 1), num_attack_types, network_config) defend_node_id = 3 defense_type = 4 max_value = 10 old_count = state.defense_values[defend_node_id][defense_type] state.defend(defend_node_id, defense_type, max_value, network_config) assert state.defense_values[defend_node_id][defense_type] < max_value assert state.defense_values[defend_node_id][ defense_type] == old_count + 1 state.defense_values[defend_node_id][defense_type] = 10 state.defend(defend_node_id, defense_type, max_value, network_config) assert state.defense_values[defend_node_id][defense_type] == max_value
def test_new_game(self): rows = 4 cols = 4 network_config = NetworkConfig(rows, cols) state = GameState() num_nodes = len(network_config.node_list) num_attack_types = 10 state.default_state(list(range(num_nodes)), (3, 1), num_attack_types, network_config) init_state = state.copy() old_game_count = state.num_games state.new_game(init_state) assert state.num_games == old_game_count + 1 assert state.done == False assert state.detected == False state.default_state(list(range(num_nodes)), (3, 1), num_attack_types, network_config) init_state = state.copy() state.hacked = True old_hacked_count = 0 state.new_game(init_state) assert state.num_hacks == old_hacked_count + 1
def get_attacker_observation( self, network_config: NetworkConfig, local_view=False, reconnaissance=False, reconnaissance_bool_features=False) -> np.ndarray: """ Converts the state of the dynamical system into an observation for the attacker. As the environment is a partially observed markov decision process, the attacker observation is only a subset of the game state :param network_config: the network configuration of the game :param local_view: boolean flag indicating whether observations are provided in a local view or not :param reconnaissance: boolean flag indicating whether reconnaissance states should be included :param reconnaissance_bool_features: boolean flag whether to include boolean features that indicate if reconnaissance have been done for a certain defense type :return: An observation of the environment """ if not reconnaissance: # +1 to have an extra feature that indicates if this is the node that the attacker is currently in attack_observation = np.zeros((len(network_config.node_list), self.attack_values.shape[1] + 1)) elif reconnaissance and not reconnaissance_bool_features: # +1 to have an extra feature that indicates if this is the node that the attacker is currently in attack_observation = np.zeros( (len(network_config.node_list), (self.attack_values.shape[1] * 2 + 1))) else: # +2 to have an extra feature that indicates if this is the node that the attacker is currently in and reconnaissance bool feature attack_observation = np.zeros( (len(network_config.node_list), (self.attack_values.shape[1] * 2 + 2))) current_pos = self.attacker_pos current_node_id = network_config.get_node_id(current_pos) current_row, current_col = current_pos current_adjacency_matrix_id = network_config.get_adjacency_matrix_id( current_row, current_col) if local_view: neighbors = [] for node_id in range(len(network_config.node_list)): pos = network_config.get_node_pos(node_id) node_row, node_col = pos node_adjacency_matrix_id = network_config.get_adjacency_matrix_id( node_row, node_col) if local_view: if network_config.adjacency_matrix[current_adjacency_matrix_id][node_adjacency_matrix_id] \ and node_id != current_node_id: if not reconnaissance: neighbor_data = np.append(self.attack_values[node_id], node_id) elif reconnaissance and not reconnaissance_bool_features: neighbor_data = np.append( np.append(self.attack_values[node_id], node_id), self.reconnaissance_state[node_id]), else: reconaissance_bool = [0] if node_id in self.reconnaissance_actions: reconaissance_bool = [1] neighbor_data = np.append( np.append( np.append(self.attack_values[node_id], node_id), self.reconnaissance_state[node_id]), reconaissance_bool) neighbor_row, neighbor_col = network_config.get_node_pos( node_id) neighbors.append( (neighbor_row, neighbor_col, neighbor_data)) else: if node_id == current_node_id: if not reconnaissance: attack_observation[node_id] = np.append( self.attack_values[node_id], 1) elif reconnaissance and not reconnaissance_bool_features: attack_observation[node_id] = np.append( np.append(self.attack_values[node_id], 1), self.reconnaissance_state[node_id]) else: reconaissance_bool = [0] if node_id in self.reconnaissance_actions: reconaissance_bool = [1] attack_observation[node_id] = np.append( np.append( np.append(self.attack_values[node_id], 1), self.reconnaissance_state[node_id]), reconaissance_bool) elif network_config.fully_observed: attack_observation[node_id] = np.append( self.attack_values[node_id], 0) elif network_config.adjacency_matrix[ current_adjacency_matrix_id][node_adjacency_matrix_id]: if not reconnaissance: attack_observation[node_id] = np.append( self.attack_values[node_id], 0) elif reconnaissance and not reconnaissance_bool_features: attack_observation[node_id] = np.append( np.append(self.attack_values[node_id], 0), self.reconnaissance_state[node_id]) else: reconaissance_bool = [0] if node_id in self.reconnaissance_actions: reconaissance_bool = [1] attack_observation[node_id] = np.append( np.append( np.append(self.attack_values[node_id], 0), self.reconnaissance_state[node_id]), reconaissance_bool) elif reconnaissance: if not reconnaissance_bool_features: attack_values = np.zeros((self.attack_values.shape[1])) attack_observation[node_id] = np.append( np.append(attack_values, 0), self.reconnaissance_state[node_id]) else: reconaissance_bool = [0] if node_id in self.reconnaissance_actions: reconaissance_bool = [1] attack_values = np.zeros((self.attack_values.shape[1])) attack_observation[node_id] = np.append( np.append(np.append(attack_values, 0), self.reconnaissance_state[node_id]), reconaissance_bool) if local_view: # sort by row then col sorted_neighbors = sorted(neighbors, key=lambda x: (x[0], x[1])) neighbor_data = np.array( list(map(lambda x: x[2], sorted_neighbors))) neighbor_ids = neighbor_data[:, self.attack_values.shape[1]] if not reconnaissance: local_view_obs = np.full((network_config.max_neighbors, self.attack_values.shape[1] + 1), -1) elif reconnaissance and not reconnaissance_bool_features: local_view_obs = np.full((network_config.max_neighbors, self.attack_values.shape[1] * 2 + 1), -1) else: local_view_obs = np.full((network_config.max_neighbors, self.attack_values.shape[1] * 2 + 2), -1) for n in range(network_config.max_neighbors): rel_neighbor_pos = network_config.relative_neighbor_positions[ n] neighbor_pos = (current_row + rel_neighbor_pos[0], current_col + rel_neighbor_pos[1]) for i in range(len(neighbor_ids)): node_id = neighbor_ids[i] node_pos = network_config.get_node_pos(node_id) if node_pos == neighbor_pos and node_pos[0] <= current_row: local_view_obs[n] = neighbor_data[i] attack_observation = np.array(local_view_obs) return attack_observation
def set_state(self, node_list: List, num_attack_types: int, defense_val: int = 2, attack_val: int = 0, num_vulnerabilities_per_node: int = 1, det_val: int = 2, vulnerability_val: int = 0, num_vulnerabilities_per_layer: int = 1, network_config: NetworkConfig = None, randomize_state: bool = False, randomize_visibility: bool = False, visibility_p: float = 0.5): """ Sets the state :param node_list: list of nodes :param num_attack_types: number of attack types :param defense_val: defense value for defense types that are not vulnerable :param attack_val: attack value for attack types :param num_vulnerabilities_per_node: number of vulnerabilities per node :param det_val: detection value per node :param vulnerability_val: defense value for defense types that are vulnerable :param num_vulnerabilities_per_layer: number of vulnerabilities per layer :param network_config: network configuration :param randomize_state: boolean flag whether to create the state randomly :param randomize_visibility: boolean flag whether to randomize visibility for partially observed envs :return: None """ num_nodes = len(node_list) attack_values = np.zeros((num_nodes, num_attack_types)) defense_values = np.zeros((num_nodes, num_attack_types)) det_values = np.zeros(num_nodes) reconnaissance_state = np.full( (num_nodes, num_attack_types), constants.GAME_CONFIG.INITIAL_RECONNAISSANCE_STATE) d_val = defense_val a_val = attack_val de_val = det_val v_val = vulnerability_val vulnerabilities_per_layer = np.zeros( (network_config.num_rows, network_config.num_cols)) for row in range(1, network_config.num_rows - 1): vulnerabilities = np.random.choice( network_config.num_cols, size=num_vulnerabilities_per_layer, replace=False) vulnerabilities_per_layer[row][vulnerabilities] = 1 for node_id in range(num_nodes): row, col = network_config.get_node_pos(node_id) num_vuln = min(num_vulnerabilities_per_node, num_attack_types) vulnerabilities = [] if vulnerabilities_per_layer[row][col] == 1 or node_list[ node_id] == NodeType.DATA.value: vulnerabilities = np.random.choice( num_attack_types, size=num_vuln) # random vulnerability per node if node_list[node_id] == NodeType.DATA.value or node_list[ node_id] == NodeType.SERVER.value: d_vals = [] a_vals = [] for at in range(num_attack_types): if randomize_state: d_val = max( self.min_random_d_val, np.random.choice( list( range(self.min_random_d_val, defense_val + 1)))) a_val = max( self.min_random_a_val, np.random.choice( list( range(self.min_random_a_val, attack_val + 1)))) de_val = max( self.min_random_det_val, np.random.choice( list( range(self.min_random_det_val, det_val + 1)))) v_val = max( vulnerability_val, np.random.choice( list( range(vulnerability_val, self.max_random_v_val + 1)))) d_vals.append(d_val) a_vals.append(a_val) defense_values[node_id] = d_vals det_values[node_id] = de_val attack_values[node_id] = a_vals for vuln_id in vulnerabilities: defense_values[node_id][ vuln_id] = v_val # vulnerability (lower defense) if randomize_visibility: for node_id in range(len(reconnaissance_state)): if np.random.rand() < visibility_p: reconnaissance_state[node_id] = defense_values[node_id] self.reconnaissance_actions.append(node_id) self.attack_values = attack_values.astype(np.int32) self.defense_values = defense_values.astype(np.int32) self.defense_det = det_values.astype(np.int32) self.reconnaissance_state = reconnaissance_state.astype(np.int32)
def test_data_col(self): num_rows = 2 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) assert network_config.data_col == 1
def test_start_row(self): num_rows = 2 num_cols = 3 network_config = NetworkConfig(num_rows, num_cols) assert network_config.start_row == 1
def __init__(self, network_config: NetworkConfig = None, manual_attacker: bool = True, num_layers: int = 1, num_servers_per_layer: int = 2, num_attack_types: int = 10, max_value: int = 9, initial_state: GameState = None, manual_defender: bool = False, initial_state_path: str = None, dense_rewards=False, min_random_a_val: int = 0, min_random_d_val: int = 0, min_random_det_val: int = 0, dense_rewards_v2=False, reconnaissance_actions: bool = False, max_random_v_val: int = 1, dense_rewards_v3=False): """ Class constructor, initializes the DTO :param network_config: the network configuration of the game (e.g. number of nodes and their connectivity) :param manual_attacker: whether the attacker is controlled manually or by an agent :param manual_attacker: whether the defender is controlled manually or by an agent :param num_layers: the number of layers in the network :param num_servers_per_layer: the number of servers per layer in the network :param num_attack_types: the number of attack types :param max_value: max value for a defense/attack attribute :param initial_state: the initial state :param initial_state_path: path to the initial state saved on disk :param dense_rewards: if true, give hacker dense rewards (reward for each intermediate server hacked) :param dense_rewards_v2: if true, give defender reward only when blocking :param min_random_a_val: minimum attack value when randomizing the state :param min_random_d_val: minimum defense value when randomizing the state :param min_random_det_val: minimum detection value when randomizing the state :param reconnaissance_actions: a boolean flag that indicates whether reconnaissance activities are enabled for the attacker :param max_random_v_val: maximum random vulnerability value when usign randomized environment """ self.reconnaissance_actions = reconnaissance_actions self.manual_attacker = manual_attacker self.manual_defender = manual_defender self.num_layers = num_layers self.num_servers_per_layer = num_servers_per_layer self.num_attack_types = num_attack_types self.max_value = max_value self.min_random_a_val = min_random_a_val self.min_random_d_val = min_random_d_val self.min_random_det_val = min_random_det_val self.num_rows = self.num_layers + 2 self.num_nodes = self.num_layers * self.num_servers_per_layer + 2 # +2 for Start and Data Nodes self.num_cols = self.num_servers_per_layer self.set_attack_actions() self.num_defense_actions = (self.num_attack_types + 1) * self.num_nodes self.num_states = self.num_nodes self.network_config = network_config self.initial_state_path = initial_state_path self.defense_val = 2 self.attack_val = 0 self.num_vulnerabilities_per_node = 1 self.det_val = 2 self.dense_rewards_v2 = dense_rewards_v2 self.dense_rewards_v3 = dense_rewards_v3 self.vulnerabilitiy_val = 0 self.max_random_v_val = max_random_v_val self.num_vulnerabilities_per_layer = None if network_config is None: self.network_config = NetworkConfig(self.num_rows, self.num_cols, connected_layers=False) self.initial_state = initial_state if self.initial_state is None and self.initial_state_path is not None: self.initial_state = GameState.load(self.initial_state) if self.initial_state is None and self.initial_state_path is None: self.initial_state = GameState( min_random_a_val=min_random_a_val, min_random_det_val=min_random_det_val, min_random_d_val=min_random_d_val, max_value=self.max_value, max_random_v_val=self.max_random_v_val) self.initial_state.default_state( self.network_config.node_list, self.network_config.start_pos, self.num_attack_types, network_config=self.network_config, ) self.dense_rewards = dense_rewards