Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1ca6d3a
Add support for topology randomization seed requests in GameCoordinator
ondrej-lukas Dec 30, 2025
6fc0c2c
Add handling for integer and None values in Action parameters
ondrej-lukas Dec 30, 2025
587981d
Fix key name for topology randomization seed in GameCoordinator
ondrej-lukas Dec 30, 2025
2b75ba2
Add seed parameter for dynamic IP change and network mapping
ondrej-lukas Dec 30, 2025
0ad4095
Refactor network mapping logic to preserve relative distances and imp…
ondrej-lukas Dec 30, 2025
92c1950
Deprecate CSV storage for replay buffer; recommend using JSONL format…
ondrej-lukas Jan 7, 2026
34ab06a
Always propagate the seed from reset to all rng processes
ondrej-lukas Mar 2, 2026
b958a35
Change signature to return Set of actions
ondrej-lukas Mar 2, 2026
ea5bf78
Add typing
ondrej-lukas Mar 2, 2026
a61c95f
Add deprecation warning to unused method
ondrej-lukas Mar 2, 2026
48c8cef
Cleanup game components
ondrej-lukas Mar 2, 2026
66350ad
remove the rest of the unused code
ondrej-lukas Mar 2, 2026
ea9bd39
Add option to pass seed during reset
ondrej-lukas Mar 2, 2026
8b40556
update class in return value of the method
ondrej-lukas Mar 2, 2026
7b18d1b
add tests for base_agent
ondrej-lukas Mar 2, 2026
7f12471
Fixed agetn tests
ondrej-lukas Mar 2, 2026
cb617d2
Fix path
ondrej-lukas Mar 2, 2026
c6a1c0e
Optimize dockerfile for size reduction
ondrej-lukas Mar 2, 2026
e53ac33
Add tests for confing parser
ondrej-lukas Mar 2, 2026
aed334f
add tests for config_manager
ondrej-lukas Mar 2, 2026
179a1ee
add .coverage
ondrej-lukas Mar 2, 2026
8661ef2
Add typing and improve method signatures
ondrej-lukas Mar 2, 2026
85f9441
Single point of seed setting
ondrej-lukas Mar 2, 2026
e675fb6
Remove old code
ondrej-lukas Mar 2, 2026
4d7ddd5
make deep copy into function
ondrej-lukas Mar 2, 2026
ed72cc8
Remove old code
ondrej-lukas Mar 2, 2026
fa0a8b3
Add seed parameter
ondrej-lukas Mar 4, 2026
7620898
Add the typing
ondrej-lukas Mar 4, 2026
6a99862
Add reset seed support
ondrej-lukas Mar 4, 2026
927971a
Use seed in the reset
ondrej-lukas Mar 4, 2026
d624357
Use seed in reset in NSG
ondrej-lukas Mar 4, 2026
52ea9e3
add typing to base agent
ondrej-lukas Mar 13, 2026
bcd3ea9
remove print
ondrej-lukas Mar 13, 2026
717900f
Add correct typing
ondrej-lukas Mar 13, 2026
2e38688
check if there is any remaining players
ondrej-lukas Mar 13, 2026
e7fd81a
Adapt processing of the requests
ondrej-lukas Mar 13, 2026
4538828
do not assignt rewards if there are no agents
ondrej-lukas Mar 13, 2026
f3af1fe
separate processing of incorrect reset requests
ondrej-lukas Mar 13, 2026
a0aa5c9
move processing of topology resets to the coordinator
ondrej-lukas Mar 13, 2026
9112fb9
Check for exact status
ondrej-lukas Mar 13, 2026
129a0ed
simplify logging
ondrej-lukas Mar 13, 2026
d925d23
Fix expected args and return values
ondrej-lukas Mar 13, 2026
cae3e47
Add check preventing sending topology randomization request w/o seed …
ondrej-lukas Mar 16, 2026
1e3f59d
add test case for the missing seed reset
ondrej-lukas Mar 16, 2026
1008bb0
Simplify scenario import
ondrej-lukas Mar 16, 2026
6b184b9
fix imports
ondrej-lukas Mar 16, 2026
fda5ca2
Fix scope of the mocks in the config parser tests
ondrej-lukas Mar 16, 2026
70d8720
Fix ruff errors
ondrej-lukas Mar 16, 2026
20e1344
Add mising import
ondrej-lukas Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.coverage
.git
.github
.gitignore
Expand All @@ -16,10 +17,9 @@ netsecgame.egg-info/
notebooks/
NetSecGameAgents/
site/
tests/
trajectories/
readme_images/
tests/
*trajectories*.json
README.md
README*.md
mkdocs.yml
30 changes: 12 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
# Use an official Python 3.12 runtime as a parent image
FROM python:3.12.10-slim
FROM python:3.12.12-slim-bookworm

# Set the working directory in the container
ENV DESTINATION_DIR=/netsecgame
WORKDIR ${DESTINATION_DIR}

# Copy the source code FIRST so pip has access to pyproject.toml
COPY . ${DESTINATION_DIR}/

# Install system dependencies
# The "Single Layer" Trick: Install tools, build app, purge tools
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --upgrade pip

COPY . ${DESTINATION_DIR}/

# Set the working directory in the container
WORKDIR ${DESTINATION_DIR}

# Install any necessary Python dependencies
# If a requirements.txt file is in the repository
RUN if [ -f pyproject.toml ]; then pip install .[server] ; fi
apt-get install -y --no-install-recommends build-essential && \
pip install --no-cache-dir --upgrade pip && \
if [ -f pyproject.toml ]; then pip install --no-cache-dir .[server] ; fi && \
apt-get purge -y --auto-remove build-essential && \
rm -rf /var/lib/apt/lists/*

ARG GAME_MODULE="netsecgame.game.worlds.NetSecGame"
# Pass the build argument to an environment variable so CMD can use it
Expand All @@ -29,8 +23,8 @@ ENV ENV_GAME_MODULE=$GAME_MODULE
# Expose the port the coordinator will run on
EXPOSE 9000

# Run the Python script when the container launches (with default arguments --task_config=netsecenv_conf.yaml --game_port=9000 --game_host=0.0.0.0)
# Run the Python script when the container launches
ENTRYPOINT ["sh", "-c", "exec python3 -m ${ENV_GAME_MODULE} --task_config=netsecenv_conf.yaml --game_port=9000 --game_host=0.0.0.0 \"$@\"", "--"]

# Default command arguments (can be overridden at runtime)
CMD ["--debug_level=INFO"]
CMD ["--debug_level=INFO"]
2 changes: 1 addition & 1 deletion NetSecGameAgents
6 changes: 2 additions & 4 deletions examples/example_task_configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ coordinator:
max_steps: 50
goal:
description: "Exfiltrate data from Samba server to remote C&C server (213.47.23.195)."
is_any_part_of_goal_random: True
known_networks: []
known_hosts: []
controlled_hosts: []
Expand All @@ -20,15 +19,14 @@ coordinator:
start_position: # Defined starting position of the attacker
known_networks: []
known_hosts: []
controlled_hosts: [213.47.23.195, random] #
controlled_hosts: [213.47.23.195, 192.168.1.1] #
known_services: {}
known_data: {}
known_blocks: {}

Defender:
goal:
description: "Block all attackers"
is_any_part_of_goal_random: False
known_networks: []
known_hosts: []
controlled_hosts: []
Expand All @@ -48,7 +46,7 @@ coordinator:
env:
scenario: 'two_networks_tiny' # use the smallest topology for this example
use_global_defender: False # Do not use global SIEM Defender
use_dynamic_addresses: False # Do not randomize IP addresses
use_dynamic_addresses: True # Do not randomize IP addresses
use_firewall: True # Use firewall
save_trajectories: False # Do not store trajectories
required_players: 1
Expand Down
38 changes: 24 additions & 14 deletions netsecgame/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import logging
import socket
import json
from abc import ABC
from abc import ABC
from typing import Optional, Tuple, Dict, Any

from netsecgame.game_components import Action, GameState, Observation, ActionType, GameStatus, AgentInfo, ProtocolConfig, AgentRole

Expand Down Expand Up @@ -55,7 +56,7 @@ def role(self)->str:
def logger(self)->logging.Logger:
return self._logger

def make_step(self, action: Action) -> Observation | None:
def make_step(self, action: Action) -> Optional[Observation]:
"""
Executes a single step in the environment by sending the agent's action to the server and receiving the resulting observation.

Expand All @@ -75,7 +76,7 @@ def make_step(self, action: Action) -> Observation | None:
else:
return None

def communicate(self, data:Action)-> tuple:
def communicate(self, data:Action)-> Tuple[GameStatus, Dict[str, Any], Optional[str]]:
"""
Exchanges data with the server and returns the server's response.
This method sends an `Action` object to the server and waits for a response.
Expand All @@ -102,7 +103,7 @@ def _send_data(socket, msg:str)->None:
self._logger.error(f'Exception in _send_data(): {e}')
raise e

def _receive_data(socket)->tuple:
def _receive_data(socket)->Tuple[GameStatus, Dict[str, Any], Optional[str]]:
"""
Receive data from server
"""
Expand Down Expand Up @@ -138,7 +139,7 @@ def _receive_data(socket)->tuple:
_send_data(self._socket, data)
return _receive_data(self._socket)

def register(self)->Observation | None:
def register(self)->Optional[Observation]:
"""
Method for registering agent to the game server.
Classname is used as agent name and the role is based on the 'role' argument.
Expand All @@ -162,19 +163,28 @@ def register(self)->Observation | None:
except Exception as e:
self._logger.error(f'Exception in register(): {e}')

def request_game_reset(self, request_trajectory=False, randomize_topology=True, randomize_topology_seed=None) -> Observation|None:
"""
Requests a game reset from the server. Optionally requests a trajectory and/or topology randomization.
def request_game_reset(
self,
request_trajectory: bool = False,
randomize_topology: bool = False,
seed: Optional[int] = None
) -> Optional[Observation]:
"""Request a game reset from the server.
Args:
request_trajectory (bool): If True, requests the server to provide a trajectory of the last episode.
randomize_topology (bool): If True, requests the server to randomize the network topology for the next episode. Defaults to True.
randomize_topology_seed (int): If provided, requests the server to use this seed for randomizing the network topology. Defaults to None.
request_trajectory: If True, requests the server to provide a
trajectory of the last episode.
randomize_topology: If True, requests the server to randomize the
network topology for the next episode. Defaults to False.
seed: If provided, requests the server to use this seed for
randomizing the environment. Required if randomize_topology is True.
Returns:
Observation: The initial observation after the reset if successful, None otherwise.
The initial observation after the reset if successful, None otherwise.
"""
if seed is None and randomize_topology:
raise ValueError("Topology randomization without seed is not supported.")
self._logger.debug("Requesting game reset")
status, observation_dict, message = self.communicate(Action(ActionType.ResetGame, parameters={"request_trajectory": request_trajectory, "randomize_topology": randomize_topology}))
if status:
status, observation_dict, message = self.communicate(Action(ActionType.ResetGame, parameters={"request_trajectory": request_trajectory, "randomize_topology": randomize_topology, "seed": seed}))
if status is GameStatus.RESET_DONE:
self._logger.debug('\tReset successful')
return Observation(GameState.from_dict(observation_dict["state"]), observation_dict["reward"], observation_dict["end"], message)
else:
Expand Down
29 changes: 10 additions & 19 deletions netsecgame/game/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
# Author: Ondrej Lukas, ondrej.lukas@aic.fel.cvut.cz

import yaml
# This is used so the agent can see the environment and game components
import importlib
from netsecgame.game_components import IP, Data, Network, Service
import netaddr
import logging
from random import randint
from typing import Optional
from netsecgame.game_components import IP, Data, Network, Service
from netsecgame.game.scenarios import SCENARIO_REGISTRY

class ConfigParser():
"""
Expand Down Expand Up @@ -386,25 +385,15 @@ def get_scenario(self):
"""
Get the scenario config objects based on the configuration. Only import objects that are selected via importlib.
"""
allowed_names = {
"scenario1" : "netsecgame.game.scenarios.scenario_configuration",
"scenario1_small" : "netsecgame.game.scenarios.smaller_scenario_configuration",
"scenario1_tiny" : "netsecgame.game.scenarios.tiny_scenario_configuration",
"one_network": "netsecgame.game.scenarios.one_net",
"three_net_scenario": "netsecgame.game.scenarios.three_net_scenario",
"two_networks": "netsecgame.game.scenarios.two_nets", # same as scenario1
"two_networks_small": "netsecgame.game.scenarios.two_nets_small", # same as scenario1_small
"two_networks_tiny": "netsecgame.game.scenarios.two_nets_tiny", # same as scenario1_small

}
scenario_name = self.config['env']['scenario']
# make sure to validate the input
if scenario_name not in allowed_names:
raise ValueError(f"Unsupported scenario: {scenario_name}")
if scenario_name not in SCENARIO_REGISTRY:
raise ValueError(
f"Unsupported scenario: {scenario_name}. "
f"Available scenarios: {list(SCENARIO_REGISTRY.keys())}"
)

# import the correct module
module = importlib.import_module(allowed_names[scenario_name])
return module.configuration_objects
return SCENARIO_REGISTRY[scenario_name]

def get_seed(self, whom):
"""
Expand All @@ -419,11 +408,13 @@ def get_randomize_goal_every_episode(self, default_value: bool = False) -> bool:
"""
Get if the randomization should be done only once or at the beginning of every episode
"""
# TODO Remove in future
try:
randomize_goal_every_episode = self.config["coordinator"]["agents"]["attackers"]["goal"]["is_any_part_of_goal_random"]
except KeyError:
# Option is not in the configuration - default to FALSE
randomize_goal_every_episode = default_value
raise DeprecationWarning("This function is deprecated.")
return randomize_goal_every_episode

def get_use_firewall(self, default_value: bool = False)->bool:
Expand Down
Loading
Loading