#2912 - eod commit. Gutted ActionManager and corrected some identifiers.
This commit is contained in:
@@ -174,4 +174,45 @@ class RansomwareLaunchC2ServerAction(AbstractAction):
|
|||||||
if config.node_name is None:
|
if config.node_name is None:
|
||||||
return ["do_nothing"]
|
return ["do_nothing"]
|
||||||
# This action currently doesn't require any further configuration options.
|
# This action currently doesn't require any further configuration options.
|
||||||
return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"]
|
return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"]
|
||||||
|
|
||||||
|
class ExfiltrationC2ServerAction(AbstractAction):
|
||||||
|
"""Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server."""
|
||||||
|
|
||||||
|
class _Opts(BaseModel):
|
||||||
|
"""Schema for options that can be passed to this action."""
|
||||||
|
|
||||||
|
username: Optional[str]
|
||||||
|
password: Optional[str]
|
||||||
|
target_ip_address: str
|
||||||
|
target_file_name: str
|
||||||
|
target_folder_name: str
|
||||||
|
exfiltration_folder_name: Optional[str]
|
||||||
|
|
||||||
|
def __init__(self, manager: "ActionManager", **kwargs) -> None:
|
||||||
|
super().__init__(manager=manager)
|
||||||
|
|
||||||
|
def form_request(
|
||||||
|
self,
|
||||||
|
node_id: int,
|
||||||
|
account: dict,
|
||||||
|
target_ip_address: str,
|
||||||
|
target_file_name: str,
|
||||||
|
target_folder_name: str,
|
||||||
|
exfiltration_folder_name: Optional[str],
|
||||||
|
) -> RequestFormat:
|
||||||
|
"""Return the action formatted as a request that can be ingested by the simulation."""
|
||||||
|
node_name = self.manager.get_node_name_by_idx(node_id)
|
||||||
|
if node_name is None:
|
||||||
|
return ["do_nothing"]
|
||||||
|
|
||||||
|
command_model = {
|
||||||
|
"target_file_name": target_file_name,
|
||||||
|
"target_folder_name": target_folder_name,
|
||||||
|
"exfiltration_folder_name": exfiltration_folder_name,
|
||||||
|
"target_ip_address": target_ip_address,
|
||||||
|
"username": account["username"],
|
||||||
|
"password": account["password"],
|
||||||
|
}
|
||||||
|
ExfiltrationC2ServerAction._Opts.model_validate(command_model)
|
||||||
|
return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model]
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||||
|
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
|
||||||
|
|
||||||
from primaite.game.agent.actions.manager import AbstractAction
|
from primaite.game.agent.actions.manager import AbstractAction
|
||||||
from primaite.interface.request import RequestFormat
|
from primaite.interface.request import RequestFormat
|
||||||
|
|
||||||
|
|||||||
@@ -268,192 +268,6 @@ class ActionManager:
|
|||||||
"""Return the gymnasium action space for this agent."""
|
"""Return the gymnasium action space for this agent."""
|
||||||
return spaces.Discrete(len(self.action_map))
|
return spaces.Discrete(len(self.action_map))
|
||||||
|
|
||||||
# def get_node_name_by_idx(self, node_idx: int) -> str:
|
|
||||||
# """
|
|
||||||
# Get the node name corresponding to the given index.
|
|
||||||
|
|
||||||
# :param node_idx: The index of the node to retrieve.
|
|
||||||
# :type node_idx: int
|
|
||||||
# :return: The node hostname.
|
|
||||||
# :rtype: str
|
|
||||||
# """
|
|
||||||
# if not node_idx < len(self.node_names):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on node {node_idx}, but its action space only"
|
|
||||||
# f"has {len(self.node_names)} nodes."
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.node_names[node_idx]
|
|
||||||
|
|
||||||
# def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]:
|
|
||||||
# """
|
|
||||||
# Get the folder name corresponding to the given node and folder indices.
|
|
||||||
|
|
||||||
# :param node_idx: The index of the node.
|
|
||||||
# :type node_idx: int
|
|
||||||
# :param folder_idx: The index of the folder on the node.
|
|
||||||
# :type folder_idx: int
|
|
||||||
# :return: The name of the folder. Or None if the node has fewer folders than the given index.
|
|
||||||
# :rtype: Optional[str]
|
|
||||||
# """
|
|
||||||
# if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this"
|
|
||||||
# f" is out of range for its action space. Folder on each node: {self.folder_names}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.folder_names[node_idx][folder_idx]
|
|
||||||
|
|
||||||
# def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]:
|
|
||||||
# """Get the file name corresponding to the given node, folder, and file indices.
|
|
||||||
|
|
||||||
# :param node_idx: The index of the node.
|
|
||||||
# :type node_idx: int
|
|
||||||
# :param folder_idx: The index of the folder on the node.
|
|
||||||
# :type folder_idx: int
|
|
||||||
# :param file_idx: The index of the file in the folder.
|
|
||||||
# :type file_idx: int
|
|
||||||
# :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has
|
|
||||||
# fewer files than the given index.
|
|
||||||
# :rtype: Optional[str]
|
|
||||||
# """
|
|
||||||
# if (
|
|
||||||
# node_idx >= len(self.file_names)
|
|
||||||
# or folder_idx >= len(self.file_names[node_idx])
|
|
||||||
# or file_idx >= len(self.file_names[node_idx][folder_idx])
|
|
||||||
# ):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}"
|
|
||||||
# f" but this is out of range for its action space. Files on each node: {self.file_names}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.file_names[node_idx][folder_idx][file_idx]
|
|
||||||
|
|
||||||
# def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]:
|
|
||||||
# """Get the service name corresponding to the given node and service indices.
|
|
||||||
|
|
||||||
# :param node_idx: The index of the node.
|
|
||||||
# :type node_idx: int
|
|
||||||
# :param service_idx: The index of the service on the node.
|
|
||||||
# :type service_idx: int
|
|
||||||
# :return: The name of the service. Or None if the node has fewer services than the given index.
|
|
||||||
# :rtype: Optional[str]
|
|
||||||
# """
|
|
||||||
# if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this"
|
|
||||||
# f" is out of range for its action space. Services on each node: {self.service_names}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.service_names[node_idx][service_idx]
|
|
||||||
|
|
||||||
# def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]:
|
|
||||||
# """Get the application name corresponding to the given node and service indices.
|
|
||||||
|
|
||||||
# :param node_idx: The index of the node.
|
|
||||||
# :type node_idx: int
|
|
||||||
# :param application_idx: The index of the service on the node.
|
|
||||||
# :type application_idx: int
|
|
||||||
# :return: The name of the service. Or None if the node has fewer services than the given index.
|
|
||||||
# :rtype: Optional[str]
|
|
||||||
# """
|
|
||||||
# if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but "
|
|
||||||
# f"this is out of range for its action space. Applications on each node: {self.application_names}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.application_names[node_idx][application_idx]
|
|
||||||
|
|
||||||
# def get_internet_protocol_by_idx(self, protocol_idx: int) -> str:
|
|
||||||
# """Get the internet protocol corresponding to the given index.
|
|
||||||
|
|
||||||
# :param protocol_idx: The index of the protocol to retrieve.
|
|
||||||
# :type protocol_idx: int
|
|
||||||
# :return: The protocol.
|
|
||||||
# :rtype: str
|
|
||||||
# """
|
|
||||||
# if protocol_idx >= len(self.protocols):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on protocol {protocol_idx} but this"
|
|
||||||
# f" is out of range for its action space. Protocols: {self.protocols}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.protocols[protocol_idx]
|
|
||||||
|
|
||||||
# def get_ip_address_by_idx(self, ip_idx: int) -> str:
|
|
||||||
# """
|
|
||||||
# Get the IP address corresponding to the given index.
|
|
||||||
|
|
||||||
# :param ip_idx: The index of the IP address to retrieve.
|
|
||||||
# :type ip_idx: int
|
|
||||||
# :return: The IP address.
|
|
||||||
# :rtype: str
|
|
||||||
# """
|
|
||||||
# if ip_idx >= len(self.ip_address_list):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on ip address {ip_idx} but this"
|
|
||||||
# f" is out of range for its action space. IP address list: {self.ip_address_list}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.ip_address_list[ip_idx]
|
|
||||||
|
|
||||||
# def get_wildcard_by_idx(self, wildcard_idx: int) -> str:
|
|
||||||
# """
|
|
||||||
# Get the IP wildcard corresponding to the given index.
|
|
||||||
|
|
||||||
# :param ip_idx: The index of the IP wildcard to retrieve.
|
|
||||||
# :type ip_idx: int
|
|
||||||
# :return: The wildcard address.
|
|
||||||
# :rtype: str
|
|
||||||
# """
|
|
||||||
# if wildcard_idx >= len(self.wildcard_list):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this"
|
|
||||||
# f" is out of range for its action space. Wildcard list: {self.wildcard_list}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.wildcard_list[wildcard_idx]
|
|
||||||
|
|
||||||
# def get_port_by_idx(self, port_idx: int) -> str:
|
|
||||||
# """
|
|
||||||
# Get the port corresponding to the given index.
|
|
||||||
|
|
||||||
# :param port_idx: The index of the port to retrieve.
|
|
||||||
# :type port_idx: int
|
|
||||||
# :return: The port.
|
|
||||||
# :rtype: str
|
|
||||||
# """
|
|
||||||
# if port_idx >= len(self.ports):
|
|
||||||
# msg = (
|
|
||||||
# f"Error: agent attempted to perform an action on port {port_idx} but this"
|
|
||||||
# f" is out of range for its action space. Port list: {self.ip_address_list}"
|
|
||||||
# )
|
|
||||||
# _LOGGER.error(msg)
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# return self.ports[port_idx]
|
|
||||||
|
|
||||||
# def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int:
|
|
||||||
# """
|
|
||||||
# Get the NIC number corresponding to the given node and NIC indices.
|
|
||||||
|
|
||||||
# :param node_idx: The index of the node.
|
|
||||||
# :type node_idx: int
|
|
||||||
# :param nic_idx: The index of the NIC on the node.
|
|
||||||
# :type nic_idx: int
|
|
||||||
# :return: The NIC number.
|
|
||||||
# :rtype: int
|
|
||||||
# """
|
|
||||||
# return nic_idx + 1
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager":
|
def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager":
|
||||||
"""
|
"""
|
||||||
|
|||||||
64
src/primaite/game/agent/actions/session.py
Normal file
64
src/primaite/game/agent/actions/session.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from primaite.game.agent.actions.manager import AbstractAction
|
||||||
|
from primaite.interface.request import RequestFormat
|
||||||
|
|
||||||
|
|
||||||
|
class NodeSessionAbstractAction(AbstractAction):
|
||||||
|
"""Base class for NodeSession actions."""
|
||||||
|
|
||||||
|
class ConfigSchema(AbstractAction.ConfigSchema):
|
||||||
|
"""Base configuration schema for NodeSessionAbstractActions."""
|
||||||
|
|
||||||
|
node_name: str
|
||||||
|
remote_ip: str
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
@classmethod
|
||||||
|
def form_request(cls, config: ConfigSchema) -> RequestFormat:
|
||||||
|
"""Abstract method. Should return the action formatted as a request which can be ingested by the PrimAITE simulation."""
|
||||||
|
if config.node_name is None or config.remote_ip is None:
|
||||||
|
return ["do_nothing"]
|
||||||
|
|
||||||
|
|
||||||
|
class NodeSessionsRemoteLoginAction(AbstractAction, identifier="node_session_remote_login"):
|
||||||
|
"""Action which performs a remote session login."""
|
||||||
|
|
||||||
|
class ConfigSchema(NodeSessionAbstractAction.ConfigSchema):
|
||||||
|
"""Configuration schema for NodeSessionsRemoteLoginAction."""
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def form_request(cls, config: ConfigSchema) -> RequestFormat:
|
||||||
|
"""Return the action formatted as a request which can be ingested by the PrimAITE simulation."""
|
||||||
|
if config.node_name is None or config.remote_ip is None:
|
||||||
|
return ["do_nothing"]
|
||||||
|
return [
|
||||||
|
"network",
|
||||||
|
"node",
|
||||||
|
config.node_name,
|
||||||
|
"service",
|
||||||
|
"Terminal",
|
||||||
|
"ssh_to_remote",
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
config.remote_ip,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NodeSessionsRemoteLogoutAction(AbstractAction, identifier="node_session_remote_logout"):
|
||||||
|
"""Action which performs a remote session logout."""
|
||||||
|
|
||||||
|
class ConfigSchema(NodeSessionAbstractAction.ConfigSchema):
|
||||||
|
"""Configuration schema for NodeSessionsRemoteLogoutAction."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def form_request(cls, config: ConfigSchema) -> RequestFormat:
|
||||||
|
"""Return the action formatted as a request which can be ingested by the PrimAITE simulation."""
|
||||||
|
if config.node_name is None or config.remote_ip is None:
|
||||||
|
return ["do_nothing"]
|
||||||
|
return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip]
|
||||||
Reference in New Issue
Block a user