Merge remote-tracking branch 'origin/dev' into feature/2689-command-and-control
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.game.agent.interface import ProxyAgent
|
||||
from primaite.game.game import PrimaiteGame
|
||||
from primaite.simulator.network.hardware.base import UserManager
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||
from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def game_and_agent_fixture(game_and_agent):
|
||||
"""Create a game with a simple agent that can be controlled by the tests."""
|
||||
game, agent = game_and_agent
|
||||
|
||||
router = game.simulation.network.get_node_by_hostname("router")
|
||||
router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=4)
|
||||
|
||||
return (game, agent)
|
||||
|
||||
|
||||
def test_remote_login(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]):
|
||||
game, agent = game_and_agent_fixture
|
||||
|
||||
server_1: Server = game.simulation.network.get_node_by_hostname("server_1")
|
||||
client_1 = game.simulation.network.get_node_by_hostname("client_1")
|
||||
|
||||
# create a new user account on server_1 that will be logged into remotely
|
||||
server_1_usm: UserManager = server_1.software_manager.software["UserManager"]
|
||||
server_1_usm.add_user("user123", "password", is_admin=True)
|
||||
|
||||
action = (
|
||||
"SSH_TO_REMOTE",
|
||||
{
|
||||
"node_id": 0,
|
||||
"username": "user123",
|
||||
"password": "password",
|
||||
"remote_ip": str(server_1.network_interface[1].ip_address),
|
||||
},
|
||||
)
|
||||
agent.store_action(action)
|
||||
game.step()
|
||||
assert agent.history[-1].response.status == "success"
|
||||
|
||||
connection_established = False
|
||||
for conn_str, conn_obj in client_1.terminal.connections.items():
|
||||
conn_obj: RemoteTerminalConnection
|
||||
if conn_obj.ip_address == server_1.network_interface[1].ip_address:
|
||||
connection_established = True
|
||||
if not connection_established:
|
||||
pytest.fail("Remote SSH connection could not be established")
|
||||
|
||||
|
||||
def test_remote_login_wrong_password(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]):
|
||||
game, agent = game_and_agent_fixture
|
||||
|
||||
server_1: Server = game.simulation.network.get_node_by_hostname("server_1")
|
||||
client_1 = game.simulation.network.get_node_by_hostname("client_1")
|
||||
|
||||
# create a new user account on server_1 that will be logged into remotely
|
||||
server_1_usm: UserManager = server_1.software_manager.software["UserManager"]
|
||||
server_1_usm.add_user("user123", "password", is_admin=True)
|
||||
|
||||
action = (
|
||||
"SSH_TO_REMOTE",
|
||||
{
|
||||
"node_id": 0,
|
||||
"username": "user123",
|
||||
"password": "wrong_password",
|
||||
"remote_ip": str(server_1.network_interface[1].ip_address),
|
||||
},
|
||||
)
|
||||
agent.store_action(action)
|
||||
game.step()
|
||||
assert agent.history[-1].response.status == "failure"
|
||||
|
||||
connection_established = False
|
||||
for conn_str, conn_obj in client_1.terminal.connections.items():
|
||||
conn_obj: RemoteTerminalConnection
|
||||
if conn_obj.ip_address == server_1.network_interface[1].ip_address:
|
||||
connection_established = True
|
||||
if connection_established:
|
||||
pytest.fail("Remote SSH connection was established despite wrong password")
|
||||
|
||||
|
||||
def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]):
|
||||
game, agent = game_and_agent_fixture
|
||||
|
||||
server_1: Server = game.simulation.network.get_node_by_hostname("server_1")
|
||||
client_1 = game.simulation.network.get_node_by_hostname("client_1")
|
||||
|
||||
# create a new user account on server_1 that will be logged into remotely
|
||||
server_1_um: UserManager = server_1.software_manager.software["UserManager"]
|
||||
server_1_um.add_user("user123", "password", is_admin=True)
|
||||
|
||||
action = (
|
||||
"NODE_ACCOUNTS_CHANGE_PASSWORD",
|
||||
{
|
||||
"node_id": 1, # server_1
|
||||
"username": "user123",
|
||||
"current_password": "password",
|
||||
"new_password": "different_password",
|
||||
},
|
||||
)
|
||||
agent.store_action(action)
|
||||
game.step()
|
||||
assert agent.history[-1].response.status == "success"
|
||||
assert server_1_um.users["user123"].password == "different_password"
|
||||
|
||||
|
||||
def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]):
|
||||
game, agent = game_and_agent_fixture
|
||||
|
||||
server_1: Server = game.simulation.network.get_node_by_hostname("server_1")
|
||||
client_1 = game.simulation.network.get_node_by_hostname("client_1")
|
||||
|
||||
# create a new user account on server_1 that will be logged into remotely
|
||||
server_1_usm: UserManager = server_1.software_manager.software["UserManager"]
|
||||
server_1_usm.add_user("user123", "password", is_admin=True)
|
||||
|
||||
# Log in remotely
|
||||
action = (
|
||||
"SSH_TO_REMOTE",
|
||||
{
|
||||
"node_id": 0,
|
||||
"username": "user123",
|
||||
"password": "password",
|
||||
"remote_ip": str(server_1.network_interface[1].ip_address),
|
||||
},
|
||||
)
|
||||
agent.store_action(action)
|
||||
game.step()
|
||||
|
||||
# Change password
|
||||
action = (
|
||||
"NODE_ACCOUNTS_CHANGE_PASSWORD",
|
||||
{
|
||||
"node_id": 1, # server_1
|
||||
"username": "user123",
|
||||
"current_password": "password",
|
||||
"new_password": "different_password",
|
||||
},
|
||||
)
|
||||
agent.store_action(action)
|
||||
game.step()
|
||||
|
||||
# Assert that the user cannot execute an action
|
||||
action = (
|
||||
"NODE_SEND_REMOTE_COMMAND",
|
||||
{
|
||||
"node_id": 0,
|
||||
"remote_ip": str(server_1.network_interface[1].ip_address),
|
||||
"command": ["file_system", "create", "file", "folder123", "doggo.pdf", False],
|
||||
},
|
||||
)
|
||||
agent.store_action(action)
|
||||
game.step()
|
||||
|
||||
assert server_1.file_system.get_folder("folder123") is None
|
||||
assert server_1.file_system.get_file("folder123", "doggo.pdf") is None
|
||||
@@ -26,6 +26,7 @@ def test_file_observation(simulation):
|
||||
dog_file_obs = FileObservation(
|
||||
where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"],
|
||||
include_num_access=False,
|
||||
file_system_requires_scan=True,
|
||||
)
|
||||
|
||||
assert dog_file_obs.space["health_status"] == spaces.Discrete(6)
|
||||
@@ -53,6 +54,7 @@ def test_folder_observation(simulation):
|
||||
root_folder_obs = FolderObservation(
|
||||
where=["network", "nodes", pc.hostname, "file_system", "folders", "test_folder"],
|
||||
include_num_access=False,
|
||||
file_system_requires_scan=True,
|
||||
num_files=1,
|
||||
files=[],
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ def test_firewall_observation():
|
||||
wildcard_list=["0.0.0.255", "0.0.0.1"],
|
||||
port_list=["HTTP", "DNS"],
|
||||
protocol_list=["TCP"],
|
||||
include_users=False,
|
||||
)
|
||||
|
||||
observation = firewall_observation.observe(firewall.describe_state())
|
||||
|
||||
@@ -38,6 +38,8 @@ def test_host_observation(simulation):
|
||||
applications=[],
|
||||
folders=[],
|
||||
network_interfaces=[],
|
||||
file_system_requires_scan=True,
|
||||
include_users=False,
|
||||
)
|
||||
|
||||
assert host_obs.space["operating_status"] == spaces.Discrete(5)
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_router_observation():
|
||||
port_list=["HTTP", "DNS"],
|
||||
protocol_list=["TCP"],
|
||||
)
|
||||
router_observation = RouterObservation(where=[], ports=ports, num_ports=8, acl=acl)
|
||||
router_observation = RouterObservation(where=[], ports=ports, num_ports=8, acl=acl, include_users=False)
|
||||
|
||||
# Observe the state using the RouterObservation instance
|
||||
observed_output = router_observation.observe(router.describe_state())
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
import pytest
|
||||
|
||||
from primaite.session.environment import PrimaiteGymEnv
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from tests import TEST_ASSETS_ROOT
|
||||
|
||||
DATA_MANIPULATION_CONFIG = TEST_ASSETS_ROOT / "configs" / "data_manipulation.yaml"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env_with_ssh() -> PrimaiteGymEnv:
|
||||
"""Build data manipulation environment with SSH port open on router."""
|
||||
env = PrimaiteGymEnv(DATA_MANIPULATION_CONFIG)
|
||||
env.agent.flatten_obs = False
|
||||
router: Router = env.game.simulation.network.get_node_by_hostname("router_1")
|
||||
router.acl.add_rule(ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=3)
|
||||
return env
|
||||
|
||||
|
||||
def extract_login_numbers_from_obs(obs):
|
||||
"""Traverse the observation dictionary and return number of user sessions for all nodes."""
|
||||
login_nums = {}
|
||||
for node_name, node_obs in obs["NODES"].items():
|
||||
login_nums[node_name] = node_obs.get("users")
|
||||
return login_nums
|
||||
|
||||
|
||||
class TestUserObservations:
|
||||
"""Test that the RouterObservation, FirewallObservation, and HostObservation have the correct number of logins."""
|
||||
|
||||
def test_no_sessions_at_episode_start(self, env_with_ssh):
|
||||
"""Test that all of the login observations start at 0 before any logins occur."""
|
||||
obs, *_ = env_with_ssh.step(0)
|
||||
logins_obs = extract_login_numbers_from_obs(obs)
|
||||
for o in logins_obs.values():
|
||||
assert o["local_login"] == 0
|
||||
assert o["remote_sessions"] == 0
|
||||
|
||||
def test_single_login(self, env_with_ssh: PrimaiteGymEnv):
|
||||
"""Test that performing a remote login increases the remote_sessions observation by 1."""
|
||||
client_1 = env_with_ssh.game.simulation.network.get_node_by_hostname("client_1")
|
||||
client_1.terminal._send_remote_login("admin", "admin", "192.168.1.14") # connect to database server via ssh
|
||||
obs, *_ = env_with_ssh.step(0)
|
||||
logins_obs = extract_login_numbers_from_obs(obs)
|
||||
db_srv_logins_obs = logins_obs.pop("HOST2") # this is the index of db server
|
||||
assert db_srv_logins_obs["local_login"] == 0
|
||||
assert db_srv_logins_obs["remote_sessions"] == 1
|
||||
for o in logins_obs.values(): # the remaining obs after popping HOST2
|
||||
assert o["local_login"] == 0
|
||||
assert o["remote_sessions"] == 0
|
||||
|
||||
def test_logout(self, env_with_ssh: PrimaiteGymEnv):
|
||||
"""Test that remote_sessions observation correctly decreases upon logout."""
|
||||
client_1 = env_with_ssh.game.simulation.network.get_node_by_hostname("client_1")
|
||||
client_1.terminal._send_remote_login("admin", "admin", "192.168.1.14") # connect to database server via ssh
|
||||
db_srv = env_with_ssh.game.simulation.network.get_node_by_hostname("database_server")
|
||||
db_srv.user_manager.change_user_password("admin", "admin", "different_pass") # changing password logs out user
|
||||
|
||||
obs, *_ = env_with_ssh.step(0)
|
||||
logins_obs = extract_login_numbers_from_obs(obs)
|
||||
for o in logins_obs.values():
|
||||
assert o["local_login"] == 0
|
||||
assert o["remote_sessions"] == 0
|
||||
|
||||
def test_max_observable_sessions(self, env_with_ssh: PrimaiteGymEnv):
|
||||
"""Log in from 5 remote places and check that only a max of 3 is shown in the observation."""
|
||||
MAX_OBSERVABLE_SESSIONS = 3
|
||||
# Right now this is hardcoded as 3 in HostObservation, FirewallObservation, and RouterObservation
|
||||
obs, *_ = env_with_ssh.step(0)
|
||||
logins_obs = extract_login_numbers_from_obs(obs)
|
||||
db_srv_logins_obs = logins_obs.pop("HOST2") # this is the index of db server
|
||||
|
||||
db_srv = env_with_ssh.game.simulation.network.get_node_by_hostname("database_server")
|
||||
db_srv.user_session_manager.remote_session_timeout_steps = 20
|
||||
db_srv.user_session_manager.max_remote_sessions = 5
|
||||
node_names = ("client_1", "client_2", "backup_server", "security_suite", "domain_controller")
|
||||
|
||||
for i, node_name in enumerate(node_names):
|
||||
node = env_with_ssh.game.simulation.network.get_node_by_hostname(node_name)
|
||||
node.terminal._send_remote_login("admin", "admin", "192.168.1.14")
|
||||
|
||||
obs, *_ = env_with_ssh.step(0)
|
||||
logins_obs = extract_login_numbers_from_obs(obs)
|
||||
db_srv_logins_obs = logins_obs.pop("HOST2") # this is the index of db server
|
||||
|
||||
assert db_srv_logins_obs["remote_sessions"] == min(MAX_OBSERVABLE_SESSIONS, i + 1)
|
||||
assert len(db_srv.user_session_manager.remote_sessions) == i + 1
|
||||
@@ -17,6 +17,7 @@ def test_file_observation():
|
||||
dog_file_obs = FileObservation(
|
||||
where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"],
|
||||
include_num_access=False,
|
||||
file_system_requires_scan=True,
|
||||
)
|
||||
assert dog_file_obs.observe(state) == {"health_status": 1}
|
||||
assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)})
|
||||
|
||||
@@ -76,13 +76,16 @@ def test_uc2_rewards(game_and_agent):
|
||||
]
|
||||
)
|
||||
state = game.get_sim_state()
|
||||
reward_value = comp.calculate(
|
||||
state,
|
||||
last_action_response=AgentHistoryItem(
|
||||
timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response
|
||||
),
|
||||
ahi = AgentHistoryItem(
|
||||
timestep=0,
|
||||
action="NODE_APPLICATION_EXECUTE",
|
||||
parameters={},
|
||||
request=["execute"],
|
||||
response=response,
|
||||
)
|
||||
reward_value = comp.calculate(state, last_action_response=ahi)
|
||||
assert reward_value == 1.0
|
||||
assert ahi.reward_info == {"last_connection_successful": True}
|
||||
|
||||
router.acl.remove_rule(position=2)
|
||||
|
||||
@@ -92,13 +95,9 @@ def test_uc2_rewards(game_and_agent):
|
||||
]
|
||||
)
|
||||
state = game.get_sim_state()
|
||||
reward_value = comp.calculate(
|
||||
state,
|
||||
last_action_response=AgentHistoryItem(
|
||||
timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response
|
||||
),
|
||||
)
|
||||
reward_value = comp.calculate(state, last_action_response=ahi)
|
||||
assert reward_value == -1.0
|
||||
assert ahi.reward_info == {"last_connection_successful": False}
|
||||
|
||||
|
||||
def test_shared_reward():
|
||||
|
||||
Reference in New Issue
Block a user