|
| 1 | +from unittest.mock import patch, MagicMock |
| 2 | +from csle_common.util.emulation_util import EmulationUtil |
| 3 | +from csle_common.dao.emulation_observation.common.emulation_connection_observation_state import ( |
| 4 | + EmulationConnectionObservationState, |
| 5 | +) |
| 6 | +from csle_common.dao.emulation_action.attacker.emulation_attacker_action_type import EmulationAttackerActionType |
| 7 | + |
| 8 | + |
| 9 | +class TestEmulationUtilSuite: |
| 10 | + """ |
| 11 | + Test suite for emulation_util |
| 12 | + """ |
| 13 | + |
| 14 | + @patch("csle_common.util.ssh_util.SSHUtil.execute_ssh_cmds") |
| 15 | + def test_execute_ssh_cmds(self, mock_execute_ssh_cmds) -> None: |
| 16 | + """ |
| 17 | + Test the method that executes a list of commands over an ssh connection to the emulation |
| 18 | +
|
| 19 | + :param mock_execute_ssh_cmds: mock_execute_ssh_cmds |
| 20 | +
|
| 21 | + :return: None |
| 22 | + """ |
| 23 | + cmds = ["ls", "pwd", "whoami"] |
| 24 | + mock_conn = MagicMock() |
| 25 | + wait_for_completion = True |
| 26 | + |
| 27 | + mock_return_value = [(b"file1\nfile2\n", b"", 0.1), (b"/home/user\n", b"", 0.05), (b"user\n", b"", 0.02)] |
| 28 | + mock_execute_ssh_cmds.return_value = mock_return_value |
| 29 | + result = EmulationUtil.execute_ssh_cmds(cmds=cmds, conn=mock_conn, wait_for_completion=wait_for_completion) |
| 30 | + mock_execute_ssh_cmds.assert_called() |
| 31 | + assert result == mock_return_value |
| 32 | + |
| 33 | + @patch("csle_common.util.ssh_util.SSHUtil.execute_ssh_cmd") |
| 34 | + def test_execute_ssh_cmd(self, mock_execute_ssh_cmd) -> None: |
| 35 | + """ |
| 36 | + Test the method that executes an action on the emulation over a ssh connection |
| 37 | +
|
| 38 | + :param mock_execute_ssh_cmd: mock_execute_ssh_cmd |
| 39 | +
|
| 40 | + :return: None |
| 41 | + """ |
| 42 | + cmd = "ls -l" |
| 43 | + mock_conn = MagicMock() |
| 44 | + wait_for_completion = True |
| 45 | + |
| 46 | + mock_return_value = (b"file1\nfile2\n", b"", 0.1) |
| 47 | + mock_execute_ssh_cmd.return_value = mock_return_value |
| 48 | + result = EmulationUtil.execute_ssh_cmd(cmd=cmd, conn=mock_conn, wait_for_completion=wait_for_completion) |
| 49 | + mock_execute_ssh_cmd.assert_called() |
| 50 | + assert result == mock_return_value |
| 51 | + |
| 52 | + def test_log_measured_action_time(self) -> None: |
| 53 | + """ |
| 54 | + Test the method that logs the measured time of an action to Kafka |
| 55 | +
|
| 56 | + :return: None |
| 57 | + """ |
| 58 | + total_time = 6 |
| 59 | + action = MagicMock() |
| 60 | + emulation_env_config = MagicMock() |
| 61 | + create_producer = True |
| 62 | + action.to_kafka_record.return_value = "mock_record" |
| 63 | + EmulationUtil.log_measured_action_time( |
| 64 | + total_time=total_time, |
| 65 | + action=action, |
| 66 | + emulation_env_config=emulation_env_config, |
| 67 | + create_producer=create_producer, |
| 68 | + ) |
| 69 | + action.to_kafka_record.assert_called() |
| 70 | + |
| 71 | + @patch("csle_common.util.ssh_util.SSHUtil.execute_ssh_cmd") |
| 72 | + def test_check_if_ssh_server_is_running_ssh(self, mock_execute_ssh_cmd) -> None: |
| 73 | + """ |
| 74 | + Test the method that checks if an ssh server is running on the machine |
| 75 | +
|
| 76 | + :param mock_execute_ssh_cmd: mock_execute_ssh_cmd |
| 77 | +
|
| 78 | + :return: None |
| 79 | + """ |
| 80 | + mock_conn = MagicMock() |
| 81 | + mock_execute_ssh_cmd.return_value = (b"sshd is running", b"", 0.1) |
| 82 | + result = EmulationUtil._check_if_ssh_server_is_running(conn=mock_conn, telnet=False) |
| 83 | + mock_execute_ssh_cmd.assert_called() |
| 84 | + assert result |
| 85 | + |
| 86 | + @patch("csle_common.util.ssh_util.SSHUtil.execute_ssh_cmd") |
| 87 | + def test_list_all_users_ssh(self, mock_execute_ssh_cmd) -> None: |
| 88 | + """ |
| 89 | + Test the method that list all users on a machine |
| 90 | +
|
| 91 | + :param mock_execute_ssh_cmd: mock_execute_ssh_cmd |
| 92 | +
|
| 93 | + :return: None |
| 94 | + """ |
| 95 | + mock_conn = MagicMock() |
| 96 | + emulation_env_config = MagicMock() |
| 97 | + c = EmulationConnectionObservationState( |
| 98 | + conn=mock_conn, credential=MagicMock(), root=False, service="ssh", port=22, ip="192.168.1.2" |
| 99 | + ) |
| 100 | + mock_execute_ssh_cmd.return_value = (b"root\nuser1\nuser2\n", b"", 0.1) |
| 101 | + result = EmulationUtil._list_all_users(c=c, emulation_env_config=emulation_env_config, telnet=False) |
| 102 | + mock_execute_ssh_cmd.assert_called() |
| 103 | + assert result == ["root", "user1", "user2"] |
| 104 | + |
| 105 | + def test_is_connection_active(self) -> None: |
| 106 | + """ |
| 107 | + Test the method that checks if a given connection is active or not |
| 108 | +
|
| 109 | + :return: None |
| 110 | + """ |
| 111 | + mock_conn = MagicMock() |
| 112 | + mock_transport = MagicMock() |
| 113 | + mock_transport.is_active.return_value = True |
| 114 | + mock_conn.get_transport.return_value = mock_transport |
| 115 | + |
| 116 | + result = EmulationUtil.is_connection_active(conn=mock_conn) |
| 117 | + assert result |
| 118 | + mock_conn.get_transport.assert_called() |
| 119 | + mock_transport.is_active.assert_called() |
| 120 | + |
| 121 | + @patch("paramiko.SSHClient") |
| 122 | + def test_setup_custom_connection(self, mock_SSHClient) -> None: |
| 123 | + """ |
| 124 | + Test utility function for setting up a custom SSH connection given credentials and a proxy connection |
| 125 | +
|
| 126 | + :param mock_SSHClient: mock_SSHClient |
| 127 | +
|
| 128 | + :return: None |
| 129 | + """ |
| 130 | + user = "user" |
| 131 | + pw = "password" |
| 132 | + source_ip = "192.168.1.1" |
| 133 | + port = 22 |
| 134 | + target_ip = "192.168.1.2" |
| 135 | + root = True |
| 136 | + |
| 137 | + mock_proxy_conn = MagicMock() |
| 138 | + mock_transport = MagicMock() |
| 139 | + mock_proxy_conn.conn.get_transport.return_value = mock_transport |
| 140 | + |
| 141 | + mock_ssh_client = MagicMock() |
| 142 | + mock_SSHClient.return_value = mock_ssh_client |
| 143 | + mock_relay_channel = MagicMock() |
| 144 | + mock_transport.open_channel.return_value = mock_relay_channel |
| 145 | + mock_transport.is_active.return_value = True |
| 146 | + |
| 147 | + result = EmulationUtil.setup_custom_connection( |
| 148 | + user=user, pw=pw, source_ip=source_ip, port=port, target_ip=target_ip, proxy_conn=mock_proxy_conn, root=root |
| 149 | + ) |
| 150 | + mock_SSHClient.assert_called() |
| 151 | + assert result |
| 152 | + |
| 153 | + def test_write_remote_file(self) -> None: |
| 154 | + """ |
| 155 | + Test utility function for writing contents to a file |
| 156 | +
|
| 157 | + :return: None |
| 158 | + """ |
| 159 | + mock_conn = MagicMock() |
| 160 | + mock_sftp_client = MagicMock() |
| 161 | + mock_remote_file = MagicMock() |
| 162 | + |
| 163 | + mock_conn.open_sftp.return_value = mock_sftp_client |
| 164 | + mock_sftp_client.file.return_value = mock_remote_file |
| 165 | + |
| 166 | + file_name = "filename.txt" |
| 167 | + contents = "content" |
| 168 | + write_mode = "w" |
| 169 | + |
| 170 | + EmulationUtil.write_remote_file(conn=mock_conn, file_name=file_name, contents=contents, write_mode=write_mode) |
| 171 | + mock_conn.open_sftp.assert_called() |
| 172 | + mock_sftp_client.file.assert_called() |
| 173 | + mock_remote_file.write.assert_called() |
| 174 | + mock_remote_file.close.assert_called() |
| 175 | + |
| 176 | + def test_connect_admin(self) -> None: |
| 177 | + """ |
| 178 | + Test the method that connects the admin agent |
| 179 | +
|
| 180 | + :return: None |
| 181 | + """ |
| 182 | + emulation_env_config = MagicMock() |
| 183 | + ip = "192.168.1.100" |
| 184 | + create_producer = True |
| 185 | + EmulationUtil.connect_admin(emulation_env_config=emulation_env_config, ip=ip, create_producer=create_producer) |
| 186 | + assert emulation_env_config.agent_ip == ip |
| 187 | + emulation_env_config.connect.assert_called() |
| 188 | + |
| 189 | + def test_disconnect_admin(self) -> None: |
| 190 | + """ |
| 191 | + Test the method that disconnects the admin agent |
| 192 | +
|
| 193 | + :return: None |
| 194 | + """ |
| 195 | + emulation_env_config = MagicMock() |
| 196 | + EmulationUtil.disconnect_admin(emulation_env_config=emulation_env_config) |
| 197 | + emulation_env_config.close_all_connections.assert_called() |
| 198 | + |
| 199 | + def test_execute_cmd_interactive_channel(self) -> None: |
| 200 | + """ |
| 201 | + Test method that executes an action on the emulation using an interactive shell (non synchronous) |
| 202 | +
|
| 203 | + :return: None |
| 204 | + """ |
| 205 | + mock_channel = MagicMock() |
| 206 | + cmd = "ls -la" |
| 207 | + EmulationUtil.execute_cmd_interactive_channel(cmd=cmd, channel=mock_channel) |
| 208 | + mock_channel.send.assert_called() |
| 209 | + |
| 210 | + @patch("csle_common.util.emulation_util.EmulationUtil.read_result_interactive_channel") |
| 211 | + def test_read_result_interactive(self, mock_read_result_interactive_channel) -> None: |
| 212 | + """ |
| 213 | + Test the method that reads the result of an action executed in interactive mode |
| 214 | +
|
| 215 | + :param mock_read_result_interactive_channel: mock_read_result_interactive_channel |
| 216 | +
|
| 217 | + :return: None |
| 218 | + """ |
| 219 | + mock_read_result_interactive_channel.return_value = "result" |
| 220 | + emulation_env_config = MagicMock() |
| 221 | + mock_channel = MagicMock() |
| 222 | + result = EmulationUtil.read_result_interactive(emulation_env_config=emulation_env_config, channel=mock_channel) |
| 223 | + assert result == "result" |
| 224 | + |
| 225 | + def test_read_result_interactive_channel(self) -> None: |
| 226 | + """ |
| 227 | + Test the method that reads the result of an action executed in interactive mode |
| 228 | +
|
| 229 | + :return: None |
| 230 | + """ |
| 231 | + emulation_env_config = MagicMock() |
| 232 | + mock_channel = MagicMock() |
| 233 | + mock_channel.recv_ready.side_effect = [False, True] |
| 234 | + mock_channel.recv.return_value = b"Mocked result" |
| 235 | + mock_shell_escape = MagicMock() |
| 236 | + mock_shell_escape.sub.return_value = "result" |
| 237 | + result = EmulationUtil.read_result_interactive_channel( |
| 238 | + emulation_env_config=emulation_env_config, channel=mock_channel |
| 239 | + ) |
| 240 | + mock_channel.recv_ready.assert_called() |
| 241 | + mock_channel.recv.assert_called() |
| 242 | + assert result |
| 243 | + |
| 244 | + def test_is_emulation_defense_action_legal(self) -> None: |
| 245 | + """ |
| 246 | + Test the method that checks if a given defense action is legal in the current state of the environment |
| 247 | +
|
| 248 | + :return: None |
| 249 | + """ |
| 250 | + defense_action_id = 1 |
| 251 | + emulation_env_config = MagicMock() |
| 252 | + emulation_env_state = MagicMock() |
| 253 | + result = EmulationUtil.is_emulation_defense_action_legal( |
| 254 | + defense_action_id=defense_action_id, env_config=emulation_env_config, env_state=emulation_env_state |
| 255 | + ) |
| 256 | + assert result |
| 257 | + |
| 258 | + @patch("csle_common.util.env_dynamics_util.EnvDynamicsUtil.logged_in_ips_str") |
| 259 | + def test_is_emulation_attack_action_legal(self, mock_logged_in_ips_str) -> None: |
| 260 | + """ |
| 261 | + Test the method that checks if a given attack action is legal in the current state of the environment |
| 262 | +
|
| 263 | + :param mock_logged_in_ips_str: mock_logged_in_ips_str |
| 264 | +
|
| 265 | + :return: None |
| 266 | + """ |
| 267 | + mock_env_config = MagicMock() |
| 268 | + mock_env_state = MagicMock() |
| 269 | + |
| 270 | + mock_env_state.attacker_action_config.actions = [MagicMock(id=0), MagicMock(id=1)] |
| 271 | + mock_env_state.attacker_obs_state = MagicMock() |
| 272 | + mock_env_state.attacker_obs_state.get_action_ips.return_value = "192.168.1.1" |
| 273 | + mock_env_state.attacker_obs_state.actions_tried = set() |
| 274 | + mock_env_state.attacker_obs_state.machines = [ |
| 275 | + MagicMock( |
| 276 | + ips="192.168.1.1", |
| 277 | + logged_in=True, |
| 278 | + root=False, |
| 279 | + filesystem_searched=False, |
| 280 | + tools_installed=False, |
| 281 | + backdoor_installed=False, |
| 282 | + untried_credentials=True, |
| 283 | + ) |
| 284 | + ] |
| 285 | + |
| 286 | + action_id = 1 |
| 287 | + mock_action = MagicMock() |
| 288 | + mock_action.id = action_id |
| 289 | + mock_action.index = 0 |
| 290 | + mock_action.type = EmulationAttackerActionType.RECON |
| 291 | + mock_env_state.attacker_action_config.actions[action_id] = mock_action |
| 292 | + |
| 293 | + result = EmulationUtil.is_emulation_attack_action_legal(action_id, mock_env_config, mock_env_state) |
| 294 | + assert not result |
| 295 | + |
| 296 | + def test_check_pid(self) -> None: |
| 297 | + """ |
| 298 | + Test the method that checks if a given pid is running on the host |
| 299 | +
|
| 300 | + :return: None |
| 301 | + """ |
| 302 | + result = EmulationUtil.check_pid(1) |
| 303 | + assert result |
| 304 | + |
| 305 | + def test_physical_ip_match(self) -> None: |
| 306 | + """ |
| 307 | + Test utility method for checking if a container ip matches a physical ip |
| 308 | +
|
| 309 | + :return: None |
| 310 | + """ |
| 311 | + emulation_env_config = MagicMock() |
| 312 | + emulation_env_config.kafka_config.container.docker_gw_bridge_ip = "192.168.0.1" |
| 313 | + emulation_env_config.kafka_config.container.physical_host_ip = "10.0.0.1" |
| 314 | + emulation_env_config.elk_config.container.docker_gw_bridge_ip = "192.168.0.2" |
| 315 | + emulation_env_config.elk_config.container.physical_host_ip = "10.0.0.2" |
| 316 | + emulation_env_config.sdn_controller_config = None |
| 317 | + |
| 318 | + mock_container = MagicMock() |
| 319 | + mock_container.physical_host_ip = "10.0.0.3" |
| 320 | + emulation_env_config.containers_config.get_container_from_ip.return_value = mock_container |
| 321 | + |
| 322 | + ip = "192.168.0.1" |
| 323 | + physical_host_ip = "10.0.0.1" |
| 324 | + |
| 325 | + result = EmulationUtil.physical_ip_match( |
| 326 | + emulation_env_config=emulation_env_config, ip=ip, physical_host_ip=physical_host_ip |
| 327 | + ) |
| 328 | + assert result |
0 commit comments