Update AINetworkDefense_Competitive/env.py
Browse files
AINetworkDefense_Competitive/env.py
CHANGED
|
@@ -5,14 +5,13 @@ from config import Config
|
|
| 5 |
|
| 6 |
class CyberPettingZooEnv(ParallelEnv):
|
| 7 |
metadata = {'render_modes': ['human'], "name": "cyber_network_defense_v2_gnn"}
|
| 8 |
-
|
| 9 |
def __init__(self):
|
| 10 |
super().__init__()
|
| 11 |
|
| 12 |
self.topology = Config.NETWORK_TOPOLOGY
|
| 13 |
self.total_nodes = Config.TOTAL_HOSTS()
|
| 14 |
-
self.features_per_node = 2
|
| 15 |
-
|
| 16 |
self.possible_agents = ["red", "blue"]
|
| 17 |
self.agents = self.possible_agents[:]
|
| 18 |
|
|
@@ -24,8 +23,8 @@ class CyberPettingZooEnv(ParallelEnv):
|
|
| 24 |
}
|
| 25 |
|
| 26 |
self.action_spaces = {
|
| 27 |
-
'red': gym.spaces.Discrete(self.total_nodes * 4),
|
| 28 |
-
'blue': gym.spaces.Discrete(self.total_nodes * 2)
|
| 29 |
}
|
| 30 |
|
| 31 |
self.node_id_to_specs = {}
|
|
@@ -43,7 +42,7 @@ class CyberPettingZooEnv(ParallelEnv):
|
|
| 43 |
node_id += 1
|
| 44 |
|
| 45 |
def _generate_static_adjacency_matrix(self):
|
| 46 |
-
"""
|
| 47 |
A = np.zeros((self.total_nodes, self.total_nodes), dtype=np.float32)
|
| 48 |
|
| 49 |
# Define cross-subnet routing permissions
|
|
@@ -63,7 +62,6 @@ class CyberPettingZooEnv(ParallelEnv):
|
|
| 63 |
if sub_j in allowed_connections[sub_i]:
|
| 64 |
A[i, j] = 1.0
|
| 65 |
|
| 66 |
-
# Add Self-Loops (A + I)
|
| 67 |
A_tilde = A + np.eye(self.total_nodes, dtype=np.float32)
|
| 68 |
|
| 69 |
# Compute Symmetric Degree Normalization:
|
|
@@ -78,6 +76,9 @@ class CyberPettingZooEnv(ParallelEnv):
|
|
| 78 |
self.agents = self.possible_agents[:]
|
| 79 |
self.state_matrix = np.zeros((self.total_nodes, self.features_per_node), dtype=np.float32)
|
| 80 |
|
|
|
|
|
|
|
|
|
|
| 81 |
obs_dict = {
|
| 82 |
'node_features': self.state_matrix.copy(),
|
| 83 |
'adjacency_matrix': self.normalized_adj.copy()
|
|
@@ -96,54 +97,77 @@ class CyberPettingZooEnv(ParallelEnv):
|
|
| 96 |
if not actions:
|
| 97 |
self.agents = []
|
| 98 |
return {}, {}, {}, {}, {}
|
| 99 |
-
|
|
|
|
|
|
|
| 100 |
red_action = actions.get('red', 0)
|
| 101 |
blue_action = actions.get('blue', 0)
|
| 102 |
-
|
| 103 |
red_target_node = red_action // 4
|
| 104 |
red_action_type = red_action % 4
|
| 105 |
blue_target_node = blue_action // 2
|
| 106 |
blue_action_type = blue_action % 2
|
| 107 |
-
|
| 108 |
red_target_node = min(max(0, red_target_node), self.total_nodes - 1)
|
| 109 |
blue_target_node = min(max(0, blue_target_node), self.total_nodes - 1)
|
| 110 |
-
|
| 111 |
red_target_specs = self.node_id_to_specs[red_target_node]
|
| 112 |
red_target_subnet = red_target_specs["subnet"]
|
| 113 |
-
|
| 114 |
-
#
|
| 115 |
if blue_action_type == 1:
|
| 116 |
self.state_matrix[blue_target_node, 1] = 1.0
|
| 117 |
-
|
| 118 |
-
#
|
| 119 |
if red_action_type == 1:
|
| 120 |
if self.state_matrix[red_target_node, 1] == 0.0:
|
| 121 |
exploit_probability = red_target_specs["base_vulnerability"]
|
| 122 |
|
| 123 |
if red_target_subnet == "secure_core":
|
| 124 |
if not self._is_subnet_compromised("active_directory"):
|
| 125 |
-
exploit_probability = 0.0
|
| 126 |
elif red_target_subnet == "active_directory":
|
| 127 |
if self._is_subnet_compromised("dns_services"):
|
| 128 |
exploit_probability = min(1.0, exploit_probability * 2.0)
|
| 129 |
-
|
| 130 |
if np.random.rand() < exploit_probability:
|
| 131 |
self.state_matrix[red_target_node, 0] = 1.0
|
| 132 |
-
|
| 133 |
-
#
|
| 134 |
total_compromised = np.sum(self.state_matrix[:, 0])
|
| 135 |
total_patched = np.sum(self.state_matrix[:, 1])
|
| 136 |
score_diff = float(total_compromised - total_patched)
|
| 137 |
progress = float(total_compromised / self.total_nodes) if self.total_nodes > 0 else 0.0
|
| 138 |
-
|
| 139 |
obs_dict = {
|
| 140 |
'node_features': self.state_matrix.copy(),
|
| 141 |
'adjacency_matrix': self.normalized_adj.copy()
|
| 142 |
}
|
| 143 |
observations = {'red': obs_dict, 'blue': obs_dict}
|
| 144 |
rewards = {'red': 0.0, 'blue': 0.0}
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
infos = {'score_difference': score_diff, 'team_progress': progress}
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
return observations, rewards, terminations, truncations, infos
|
|
|
|
| 5 |
|
| 6 |
class CyberPettingZooEnv(ParallelEnv):
|
| 7 |
metadata = {'render_modes': ['human'], "name": "cyber_network_defense_v2_gnn"}
|
| 8 |
+
|
| 9 |
def __init__(self):
|
| 10 |
super().__init__()
|
| 11 |
|
| 12 |
self.topology = Config.NETWORK_TOPOLOGY
|
| 13 |
self.total_nodes = Config.TOTAL_HOSTS()
|
| 14 |
+
self.features_per_node = 2
|
|
|
|
| 15 |
self.possible_agents = ["red", "blue"]
|
| 16 |
self.agents = self.possible_agents[:]
|
| 17 |
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
self.action_spaces = {
|
| 26 |
+
'red': gym.spaces.Discrete(self.total_nodes * 4),
|
| 27 |
+
'blue': gym.spaces.Discrete(self.total_nodes * 2)
|
| 28 |
}
|
| 29 |
|
| 30 |
self.node_id_to_specs = {}
|
|
|
|
| 42 |
node_id += 1
|
| 43 |
|
| 44 |
def _generate_static_adjacency_matrix(self):
|
| 45 |
+
""" GNN CORE: Programmatically wires lateral communication pathways between subnets."""
|
| 46 |
A = np.zeros((self.total_nodes, self.total_nodes), dtype=np.float32)
|
| 47 |
|
| 48 |
# Define cross-subnet routing permissions
|
|
|
|
| 62 |
if sub_j in allowed_connections[sub_i]:
|
| 63 |
A[i, j] = 1.0
|
| 64 |
|
|
|
|
| 65 |
A_tilde = A + np.eye(self.total_nodes, dtype=np.float32)
|
| 66 |
|
| 67 |
# Compute Symmetric Degree Normalization:
|
|
|
|
| 76 |
self.agents = self.possible_agents[:]
|
| 77 |
self.state_matrix = np.zeros((self.total_nodes, self.features_per_node), dtype=np.float32)
|
| 78 |
|
| 79 |
+
# Initialize step tracker
|
| 80 |
+
self.current_step = 0
|
| 81 |
+
|
| 82 |
obs_dict = {
|
| 83 |
'node_features': self.state_matrix.copy(),
|
| 84 |
'adjacency_matrix': self.normalized_adj.copy()
|
|
|
|
| 97 |
if not actions:
|
| 98 |
self.agents = []
|
| 99 |
return {}, {}, {}, {}, {}
|
| 100 |
+
|
| 101 |
+
self.current_step += 1
|
| 102 |
+
|
| 103 |
red_action = actions.get('red', 0)
|
| 104 |
blue_action = actions.get('blue', 0)
|
| 105 |
+
|
| 106 |
red_target_node = red_action // 4
|
| 107 |
red_action_type = red_action % 4
|
| 108 |
blue_target_node = blue_action // 2
|
| 109 |
blue_action_type = blue_action % 2
|
| 110 |
+
|
| 111 |
red_target_node = min(max(0, red_target_node), self.total_nodes - 1)
|
| 112 |
blue_target_node = min(max(0, blue_target_node), self.total_nodes - 1)
|
| 113 |
+
|
| 114 |
red_target_specs = self.node_id_to_specs[red_target_node]
|
| 115 |
red_target_subnet = red_target_specs["subnet"]
|
| 116 |
+
|
| 117 |
+
# Defense Execution
|
| 118 |
if blue_action_type == 1:
|
| 119 |
self.state_matrix[blue_target_node, 1] = 1.0
|
| 120 |
+
|
| 121 |
+
# Attack Execution with Infrastructure Co-Dependencies
|
| 122 |
if red_action_type == 1:
|
| 123 |
if self.state_matrix[red_target_node, 1] == 0.0:
|
| 124 |
exploit_probability = red_target_specs["base_vulnerability"]
|
| 125 |
|
| 126 |
if red_target_subnet == "secure_core":
|
| 127 |
if not self._is_subnet_compromised("active_directory"):
|
| 128 |
+
exploit_probability = 0.0
|
| 129 |
elif red_target_subnet == "active_directory":
|
| 130 |
if self._is_subnet_compromised("dns_services"):
|
| 131 |
exploit_probability = min(1.0, exploit_probability * 2.0)
|
| 132 |
+
|
| 133 |
if np.random.rand() < exploit_probability:
|
| 134 |
self.state_matrix[red_target_node, 0] = 1.0
|
| 135 |
+
|
| 136 |
+
# Telemetry Preparation
|
| 137 |
total_compromised = np.sum(self.state_matrix[:, 0])
|
| 138 |
total_patched = np.sum(self.state_matrix[:, 1])
|
| 139 |
score_diff = float(total_compromised - total_patched)
|
| 140 |
progress = float(total_compromised / self.total_nodes) if self.total_nodes > 0 else 0.0
|
| 141 |
+
|
| 142 |
obs_dict = {
|
| 143 |
'node_features': self.state_matrix.copy(),
|
| 144 |
'adjacency_matrix': self.normalized_adj.copy()
|
| 145 |
}
|
| 146 |
observations = {'red': obs_dict, 'blue': obs_dict}
|
| 147 |
rewards = {'red': 0.0, 'blue': 0.0}
|
| 148 |
+
|
| 149 |
+
# DYNAMIC EPISODE TERMINATIONS AND TRUNCATIONS
|
| 150 |
+
reached_max_steps = self.current_step >= Config.MAX_STEPS_PER_EPISODE
|
| 151 |
+
|
| 152 |
+
if str(Config.MODE).lower() == "cooperative":
|
| 153 |
+
# Cooperative: Runs the full duration of the step limit
|
| 154 |
+
is_terminated = False
|
| 155 |
+
is_truncated = reached_max_steps
|
| 156 |
+
else:
|
| 157 |
+
# Competitive: Allow early wins (e.g.Red conquers all, or Blue erases threat)
|
| 158 |
+
red_won = (total_compromised == self.total_nodes)
|
| 159 |
+
blue_won = (total_compromised == 0 and self.current_step > 5)
|
| 160 |
+
|
| 161 |
+
is_terminated = red_won or blue_won
|
| 162 |
+
is_truncated = reached_max_steps
|
| 163 |
+
|
| 164 |
+
terminations = {agent: is_terminated for agent in self.possible_agents}
|
| 165 |
+
truncations = {agent: is_truncated for agent in self.possible_agents}
|
| 166 |
+
|
| 167 |
infos = {'score_difference': score_diff, 'team_progress': progress}
|
| 168 |
+
|
| 169 |
+
# Cleanup agent lists if active sequence ends
|
| 170 |
+
if is_terminated or is_truncated:
|
| 171 |
+
self.agents = []
|
| 172 |
+
|
| 173 |
return observations, rewards, terminations, truncations, infos
|