Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions benchmarks/configs/arena_2026_dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
arena: 2026
split: dev
difficulty: medium
max_ticks: 200
seeds: [9001, 9002, 9003, 9004, 9005]
scenarios: [ransomware, apt_espionage, cloud_hybrid, iot_grid, ot_stuxnet]
baselines: [random, heuristic-blue, killchain-red]
metrics: [mission_success, sla_uptime, security, false_positives, catastrophic_failure, cvar05_blue_return]
compute_budget_env_steps: 1000000
2 changes: 1 addition & 1 deletion benchmarks/results/leaderboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
{
"name": "smoke",
"team": "blue",
"submitted_at": "2026-07-11T12:39:14Z",
"submitted_at": "2026-09-20T11:00:29Z",
"n_episodes": 1,
"score": 50.0033,
"mean_compromised": 0.0,
Expand Down
7 changes: 7 additions & 0 deletions examples/my_scenario.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: my_experiment
base: ransomware
difficulty: medium
max_ticks: 200
description: Minimal custom experiment — no simulator code changes.
reward_variant: constrained_sla
log_latency: 2
19 changes: 19 additions & 0 deletions examples/submissions/heuristic_blue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""CAGE-style ``make_blue`` example.

netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 --max-ticks 20
"""

NAME = 'heuristic-blue'
TEAM = 'netforge-baselines'
TECHNIQUE = 'scripted-siem-isolate'
PAPER = ''


def wrap(env):
return env


def make_blue(seed=0):
from netforge_rl.baselines.policies import HeuristicBluePolicy

return HeuristicBluePolicy(seed=seed)
1 change: 1 addition & 0 deletions netforge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""CLI entry so ``python -m netforge`` matches the ``netforge`` script."""
4 changes: 4 additions & 0 deletions netforge/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from netforge_rl.cli import main

if __name__ == '__main__':
raise SystemExit(main())
24 changes: 18 additions & 6 deletions netforge_rl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
from .environment.parallel_env import NetForgeRLEnv
from .core.action import BaseAction, ActionEffect
from .core.state import GlobalNetworkState, Host, Subnet
from .core.action import ActionEffect, BaseAction
from .core.observation import BaseObservation
from .core.state import GlobalNetworkState, Host, Subnet
from .environment.parallel_env import NetForgeRLEnv

__version__ = '4.0.0'

__all__ = [
'NetForgeRLEnv',
'BaseAction',
'ActionEffect',
'BaseAction',
'BaseObservation',
'GlobalNetworkState',
'Host',
'NetForgeRLEnv',
'Subnet',
'BaseObservation',
'__version__',
]


def _register_third_party_envs() -> None:
from netforge_rl.environment.registry import register_envs

register_envs()


_register_third_party_envs()
4 changes: 4 additions & 0 deletions netforge_rl/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from netforge_rl.cli import main

if __name__ == '__main__':
raise SystemExit(main())
80 changes: 41 additions & 39 deletions netforge_rl/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,76 @@
from .blue import (
IsolateHost,
RestoreHost,
Monitor,
Analyze,
DeployDecoy,
Remove,
RestoreFromBackup,
ConfigureACL,
DecoyApache,
DecoySSHD,
DecoyTomcat,
Misinform,
ConfigureACL,
SecurityAwarenessTraining,
DeployDecoy,
DeployEDR,
DeployHoneytoken,
IsolateHost,
Misinform,
Monitor,
Remove,
RestoreFromBackup,
RestoreHost,
RotateKerberos,
SecurityAwarenessTraining,
)
from .network.ip_fragmentation import IPFragmentationAction
from .red import (
NetworkScan,
DiscoverRemoteSystems,
DiscoverNetworkServices,
ExploitRemoteService,
PrivilegeEscalate,
Impact,
DiscoverRemoteSystems,
ExploitBlueKeep,
ExploitEternalBlue,
ExploitHTTP_RFI,
ExploitRemoteService,
Impact,
JuicyPotato,
V4L2KernelExploit,
KillProcess,
ShareIntelligence,
NetworkScan,
OverloadPLC,
PrivilegeEscalate,
ShareIntelligence,
SpearPhishing,
V4L2KernelExploit,
)
from .red.post_exploitation import DumpLSASS, PassTheTicket
from .network.ip_fragmentation import IPFragmentationAction

__all__ = [
'IsolateHost',
'RestoreHost',
'Monitor',
'Analyze',
'DeployDecoy',
'Remove',
'RestoreFromBackup',
'ConfigureACL',
'DecoyApache',
'DecoySSHD',
'DecoyTomcat',
'Misinform',
'ConfigureACL',
'SecurityAwarenessTraining',
'DeployDecoy',
'DeployEDR',
'DeployHoneytoken',
'RotateKerberos',
'NetworkScan',
'DiscoverRemoteSystems',
'DiscoverNetworkServices',
'ExploitRemoteService',
'PrivilegeEscalate',
'Impact',
'DiscoverRemoteSystems',
'DumpLSASS',
'ExfiltrateData',
'ExploitBlueKeep',
'ExploitEternalBlue',
'ExploitHTTP_RFI',
'ExploitRemoteService',
'IPFragmentationAction',
'Impact',
'IsolateHost',
'JuicyPotato',
'V4L2KernelExploit',
'KillProcess',
'ShareIntelligence',
'Misinform',
'Monitor',
'NetworkScan',
'OverloadPLC',
'SpearPhishing',
'DumpLSASS',
'PassTheTicket',
'PassTheHash',
'ExfiltrateData',
'IPFragmentationAction',
'PassTheTicket',
'PrivilegeEscalate',
'Remove',
'RestoreFromBackup',
'RestoreHost',
'RotateKerberos',
'SecurityAwarenessTraining',
'ShareIntelligence',
'SpearPhishing',
'V4L2KernelExploit',
]
42 changes: 22 additions & 20 deletions netforge_rl/actions/blue/__init__.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
from .mitigation import (
IsolateHost,
RestoreHost,
Remove,
RestoreFromBackup,
ConfigureACL,
SecurityAwarenessTraining,
)
from .analysis import Monitor, Analyze
from .analysis import Analyze, Monitor
from .deception import (
DeployDecoy,
DecoyApache,
DecoySSHD,
DecoyTomcat,
Misinform,
DeployDecoy,
DeployHoneytoken,
Misinform,
)
from .edr import DeployEDR
from .mitigation import (
ConfigureACL,
IsolateHost,
Remove,
RestoreFromBackup,
RestoreHost,
SecurityAwarenessTraining,
)

__all__ = [
'IsolateHost',
'RestoreHost',
'Remove',
'RestoreFromBackup',
'Monitor',
'Analyze',
'DeployDecoy',
'ConfigureACL',
'DecoyApache',
'DecoySSHD',
'DecoyTomcat',
'Misinform',
'ConfigureACL',
'SecurityAwarenessTraining',
'DeployDecoy',
'DeployEDR',
'DeployHoneytoken',
'IsolateHost',
'Misinform',
'Monitor',
'Remove',
'RestoreFromBackup',
'RestoreHost',
'RotateKerberos',
'SecurityAwarenessTraining',
]
from .identity import RotateKerberos
10 changes: 6 additions & 4 deletions netforge_rl/agents/rule_based_blue.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from __future__ import annotations

import random
from typing import Any, Optional
from netforge_rl.actions.blue.mitigation import IsolateHost, RestoreHost

from netforge_rl.actions.blue.analysis import Analyze, Monitor
from netforge_rl.actions.blue.deception import DeployHoneytoken
from netforge_rl.actions.blue.edr import DeployEDR
from netforge_rl.actions.blue.mitigation import IsolateHost, RestoreHost


class RuleBasedBlueAgent:
"""Deterministic Blue defender for use as a lower-bound paper baseline."""
"""Deterministic Blue lower bound."""

def __init__(self, agent_id: str):
self.agent_id = agent_id
Expand All @@ -20,7 +22,7 @@ def reset(self) -> None:
self._honeytokened.clear()
self._edr_deployed.clear()

def get_action(self, _observation: Any, global_state) -> Optional[Any]:
def get_action(self, _observation, global_state):
live = {
ip: h
for ip, h in global_state.all_hosts.items()
Expand Down
45 changes: 45 additions & 0 deletions netforge_rl/arena/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from netforge_rl.arena.adversarial import evaluate_population
from netforge_rl.arena.agent_card import build_agent_card, format_agent_card
from netforge_rl.arena.constraints import ConstrainedEnvWrapper
from netforge_rl.arena.evaluate import evaluate_split, run_episode
from netforge_rl.arena.leaderboard import (
format_leaderboard,
load_leaderboard,
save_leaderboard,
)
from netforge_rl.arena.metrics import ArenaMetrics, aggregate, cvar
from netforge_rl.arena.ood import evaluate_ood
from netforge_rl.arena.questions import (
QUESTIONS,
get_question,
list_questions,
run_question,
)
from netforge_rl.arena.spec import ARENA_2026, ARENA_NAME, ArenaSpec, current_arena
from netforge_rl.arena.submissions import evaluate_submission, load_submission

__all__ = [
'ARENA_2026',
'ARENA_NAME',
'QUESTIONS',
'ArenaMetrics',
'ArenaSpec',
'ConstrainedEnvWrapper',
'aggregate',
'build_agent_card',
'current_arena',
'cvar',
'evaluate_ood',
'evaluate_population',
'evaluate_split',
'evaluate_submission',
'format_agent_card',
'format_leaderboard',
'get_question',
'list_questions',
'load_leaderboard',
'load_submission',
'run_episode',
'run_question',
'save_leaderboard',
]
51 changes: 51 additions & 0 deletions netforge_rl/arena/adversarial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from collections.abc import Callable, Mapping, Sequence

from netforge_rl.arena.evaluate import evaluate_split
from netforge_rl.arena.spec import ARENA_2026
from netforge_rl.baselines.policies import (
HeuristicRedPolicy,
KillChainRedPolicy,
RandomPolicy,
)

DEFAULT_RED_POPULATION: dict[str, Callable] = {
'random-red': lambda: RandomPolicy(seed=0),
'heuristic-red': lambda: HeuristicRedPolicy(seed=0),
'killchain-red': lambda: KillChainRedPolicy(seed=0),
}


def evaluate_population(
blue_policy_factory: Callable,
red_population: Mapping[str, Callable] | None = None,
*,
scenarios: Sequence[str] = ('ransomware',),
seeds: Sequence[int] = (0, 1),
max_ticks: int = 60,
) -> dict:
"""Evaluate one Blue policy against a population of Red opponents."""
pool = dict(red_population or DEFAULT_RED_POPULATION)
per_red = {}
for name, factory in pool.items():
per_red[name] = evaluate_split(
factory,
blue_policy_factory,
split='dev',
spec=ARENA_2026,
scenarios=scenarios,
seeds=seeds,
max_ticks=max_ticks,
)
sla = [
v['overall']['mean']['sla_uptime']
for v in per_red.values()
if v['overall']['n_episodes']
]
return {
'n_opponents': len(pool),
'per_red': per_red,
'mean_sla_across_red': round(sum(sla) / max(len(sla), 1), 4),
'worst_sla_across_red': round(min(sla) if sla else 0.0, 4),
}
Loading
Loading