From 526b695f837ee9adc4bc562b9ec4351f15898353 Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:15:48 +0200 Subject: [PATCH 1/8] Replace MkDocs with Sphinx so the gym docs can ship as HTML. Stop ignoring the docs tree so Sphinx sources are versioned; keep docs/_build and site/ out of git. Co-authored-by: Cursor --- .gitignore | 11 +-- docs/Makefile | 12 +++ docs/README.md | 15 ++++ docs/conf.py | 125 ++++++++++++++++++++++++++++++ docs/quickstart.md | 147 ------------------------------------ docs/requirements.txt | 11 +++ docs/stylesheets/custom.css | 107 -------------------------- mkdocs.yml | 128 ------------------------------- 8 files changed, 164 insertions(+), 392 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/conf.py delete mode 100644 docs/quickstart.md create mode 100644 docs/requirements.txt delete mode 100644 docs/stylesheets/custom.css delete mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index 852c086..affc2ec 100644 --- a/.gitignore +++ b/.gitignore @@ -26,16 +26,7 @@ CybORG/Emulator/Logs/ CybORG/Emulator/logs/ CybORG/Emulator/Config/emulatorconfig.ini docs/_build/ +site/ dist -CybORG/Shared/Config/config.ini - -CybORG/Emulator/AWS/Config/awsconfig.ini - -*.swp -*.pstats - -.ipynb_checkpoints/ -docs/ - .ruff_cache/ \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..68d898c --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,12 @@ +SPHINXOPTS ?= -W --keep-going +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: help Makefile + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9b127a0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# Docs map + +| | | +|---|---| +| First episode | `README.md`, {doc}`getting-started` | +| Env ids | {doc}`python-quickstart` | +| Red / Blue | {doc}`cybersec/overview` | +| Package | `netforge_rl/` | +| Figures | `docs/_static/figures/*.svg` + `*.excalidraw` | + +```bash +python docs/_static/figures/build.py +pip install -e ".[docs]" +sphinx-build -b html -W --keep-going docs docs/_build/html +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..4c5a684 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,125 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +project = 'NetForge' +author = 'Igor Jankowski' +copyright = '2026, Igor Jankowski' +release = '4.0.0' + +extensions = [ + 'myst_parser', + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinxcontrib.mermaid', +] + +myst_enable_extensions = ['colon_fence', 'deflist', 'dollarmath'] +myst_heading_anchors = 2 +myst_fence_as_directive = ['mermaid'] +myst_suppress_warnings = ['myst.header'] +source_suffix = {'.md': 'markdown', '.rst': 'restructuredtext'} + +html_theme = 'furo' +html_title = 'NetForge' +html_logo = '_static/figures/mark.svg' +html_static_path = ['_static'] +templates_path = ['_templates'] +html_css_files = ['custom.css'] +html_js_files = ['custom.js'] +html_copy_source = False +html_show_sphinx = False + +html_theme_options = { + 'navigation_with_keys': True, + 'source_repository': 'https://github.com/reforcemind/NetForge_RL/', + 'source_branch': 'main', + 'source_directory': 'docs/', + 'footer_icons': [ + { + 'name': 'GitHub', + 'url': 'https://github.com/reforcemind/NetForge_RL', + 'html': """ + +""", + 'class': '', + }, + ], + 'light_css_variables': { + 'color-brand-primary': '#7b2733', + 'color-brand-content': '#7b2733', + 'color-background-primary': '#f7efdc', + 'color-background-secondary': '#f1e6cc', + 'color-background-hover': '#ece0c4', + 'color-background-border': '#e2d4b6', + 'color-foreground-primary': '#2b2521', + 'color-foreground-secondary': '#5c5248', + 'color-foreground-muted': '#8a7d6d', + 'color-code-background': '#f2e8d0', + 'color-code-foreground': '#5e1f2b', + 'color-admonition-background': '#f1e6cc', + 'color-highlight-on-target': '#fcefd4', + 'font-stack': "'Libre Franklin', ui-sans-serif, system-ui, sans-serif", + 'font-stack--monospace': "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace", + 'font-stack--headings': "'Newsreader', Georgia, 'Times New Roman', serif", + 'sidebar-caption-font-size': '0.72rem', + }, + 'dark_css_variables': { + 'color-brand-primary': '#dd94a0', + 'color-brand-content': '#e3a6b0', + 'color-background-primary': '#211b19', + 'color-background-secondary': '#2a2320', + 'color-background-hover': '#342a25', + 'color-background-border': '#3b302b', + 'color-foreground-primary': '#ece0cb', + 'color-foreground-secondary': '#c9bba6', + 'color-foreground-muted': '#9a8b78', + 'color-code-background': '#2a2320', + 'color-code-foreground': '#e6c9b8', + 'color-admonition-background': '#2a2320', + 'font-stack': "'Libre Franklin', ui-sans-serif, system-ui, sans-serif", + 'font-stack--monospace': "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace", + 'font-stack--headings': "'Newsreader', Georgia, 'Times New Roman', serif", + }, +} + +exclude_patterns = [ + '_build', + 'README.md', + 'stylesheets', +] + +autodoc_mock_imports = [ + 'jax', + 'jaxlib', + 'ray', + 'torch', + 'transformers', + 'sentence_transformers', + 'moviepy', + 'docker', + 'scapy', + 'minari', + 'h5py', + 'matplotlib', + 'stable_baselines3', +] +napoleon_google_docstring = True +autodoc_member_order = 'bysource' +autodoc_default_options = { + 'members': True, + 'undoc-members': False, + 'show-inheritance': True, +} + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'numpy': ('https://numpy.org/doc/stable/', None), + 'gymnasium': ('https://gymnasium.farama.org/', None), +} + +nitpicky = False +suppress_warnings = ['myst.header'] diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index 5906642..0000000 --- a/docs/quickstart.md +++ /dev/null @@ -1,147 +0,0 @@ -# Quick Start Guide - -## Installation - -Requires Python 3.12+. - -```bash -pip install 'netforge_rl[jax,render,finetune] @ git+https://github.com/reforcemind/NetForge_RL' -``` - -Extras: `jax` (vectorized backend), `render` (matplotlib + moviepy), `finetune` -(TRL/PEFT for LLM agents), `rllib` (Ray RLlib bridge). - -## PettingZoo execution (single environment) - -The reference Python backend implements the PettingZoo parallel API. Each agent acts with a -`MultiDiscrete([32, 100])` action — `[action_type_id, target_host_index]`. - -```python -import numpy as np -from netforge_rl.environment.parallel_env import NetForgeRLEnv - -env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 50}) -obs, infos = env.reset(seed=0) - -while env.agents: - actions = {a: env.action_space(a).sample() for a in env.agents} - obs, rewards, term, trunc, infos = env.step(actions) - if all(term.values()) or all(trunc.values()): - break -``` - -Each `obs[agent]` is a dict with `obs` (256-d state), `action_mask` (132-d), `siem_embedding` -(128-d), `adj_matrix`, and `delta_t`; Blue agents additionally get `blue_comm`. - -## Difficulty presets and the held-out split - -Instead of hand-writing config dicts, use named difficulty tiers and the frozen evaluation -seed suite. See [Difficulty & Splits](environment/difficulty.md) for the full knob table. - -```python -from netforge_rl.environment import make_env, EVAL_SEEDS - -# Train on any seeds at a chosen difficulty -train_env = make_env('medium', scenario_type='apt_espionage', seed=0) - -# Report on held-out topologies never seen during training -eval_env = make_env('hard', scenario_type='apt_espionage', evaluation=True, seed=EVAL_SEEDS[0]) -``` - -## JAX vectorized execution - -Hardware-accelerated batched rollouts for high-throughput training. - -```python -import jax -from netforge_rl.backends.jax import VectorEnvSpec -from netforge_rl.bridges.jaxmarl import JaxMARLEnv, random_action_dict - -env = JaxMARLEnv(spec=VectorEnvSpec(n_hosts=100, n_red=1, n_blue=3), batch_size=4096) -key = jax.random.PRNGKey(0) - -obs, state = env.reset(key) -obs, state, reward, done, info = env.step(key, state, random_action_dict(env, key)) -``` - -## Running baselines - -```python -from netforge_rl.environment.parallel_env import NetForgeRLEnv -from netforge_rl.baselines.policies import KillChainRedPolicy, HeuristicBluePolicy - -env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 150}) -env.reset(seed=0) -red, blue = KillChainRedPolicy(seed=0), HeuristicBluePolicy(seed=0) - -while env.agents: - actions = {a: (red.act(env, a) if 'red' in a else blue.act(env, a)) for a in env.agents} - _, _, term, trunc, _ = env.step(actions) - if all(term.values()) or all(trunc.values()): - break -``` - -`KillChainRedPolicy` runs a recon → exploit → pivot kill-chain and actually compromises -hosts; see [Baselines](benchmarks/baselines.md). - -## Single-agent training (Gymnasium / SB3 / CleanRL) - -For standard single-agent RL, `NetForgeSingleAgentEnv` is a `gymnasium.Env` that controls one -agent against scripted opponents. It passes `gymnasium`'s `check_env` and exposes the action -mask in `info` for maskable algorithms. - -```python -from netforge_rl.environment import NetForgeSingleAgentEnv -from netforge_rl.baselines.policies import KillChainRedPolicy - -env = NetForgeSingleAgentEnv( - 'ransomware', - controlled_agent='blue_dmz', - opponents={'red_operator': KillChainRedPolicy(seed=0)}, -) -obs, info = env.reset(seed=0) # obs: Box(384,), info['action_mask']: (132,) -obs, reward, terminated, truncated, info = env.step(env.action_space.sample()) -``` - -## Training IPPO on the JAX backend - -```python -from netforge_rl.baselines.jax_ppo import ippo_train, PPOConfig, save_params - -out = ippo_train(PPOConfig(total_iters=40, num_steps=48, batch_size=128)) -save_params(out['params'], 'runs/ippo_blue.npz') -print('reward:', out['reward_curve'][0], '->', out['reward_curve'][-1]) -``` - -See [Baselines](benchmarks/baselines.md) for the committed learning curve. - -## Running the diagnostic probes - -```python -from netforge_rl.diagnostics import all_diagnostics, run_diagnostic -from netforge_rl.baselines.policies import HeuristicBluePolicy - -for probe in all_diagnostics(): - result = run_diagnostic(probe, HeuristicBluePolicy(seed=0), seed=0) - print(f'{probe.capability:14} {probe.name:26} score={result.score:.2f}') -``` - -See [Diagnostics](diagnostics/overview.md) for what each capability measures. - -## LLM SOC agent (optional) - -`run_episode` takes a `clients` dict mapping each agent id to an LLM client; uncontrolled -agents no-op, and unparseable replies are counted as `invalid_replies`. - -```python -from netforge_rl.environment.parallel_env import NetForgeRLEnv -from netforge_rl.semantic.clients.mock import MockLLMClient -from netforge_rl.semantic.runner import run_episode - -env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 50}) -result = run_episode(env, clients={'blue_dmz': MockLLMClient()}, seed=0) -print(result.rewards, result.invalid_replies) -``` - -Swap `MockLLMClient` for an OpenAI/Anthropic/vLLM client to have a language model read raw -SIEM logs and issue defensive actions. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..f37ea70 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,11 @@ +sphinx>=8.0 +furo>=2024.8.6 +myst-parser>=4.0 +sphinxcontrib-mermaid>=1.0.0 +numpy>=2.0 +gymnasium>=1.2 +pettingzoo>=1.26 +networkx>=3.4 +pyyaml>=6.0 +pillow>=10.0 +scikit-learn>=1.0 diff --git a/docs/stylesheets/custom.css b/docs/stylesheets/custom.css deleted file mode 100644 index eec8751..0000000 --- a/docs/stylesheets/custom.css +++ /dev/null @@ -1,107 +0,0 @@ -/* ReforceMind Target Site Emulation */ - -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Libre+Franklin:wght@400;500;600;700&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600;6..72,700&display=swap'); - -:root { - /* Colors based on the reforcemind.github.io parchment/ink palette */ - --md-primary-fg-color: #1a4231; /* forest-intellect */ - --md-accent-fg-color: #8b2635; /* crimson-authority */ - - --md-default-bg-color: #F8F5F0; /* parchment-base */ - --md-default-fg-color: #1A1D20; /* ink-black */ - - --md-code-bg-color: #e9e5de; - --md-code-fg-color: #1A1D20; -} - -[data-md-color-scheme="slate"] { - --md-default-bg-color: #0b0f19; - --md-default-fg-color: #e2e8f0; - --md-code-bg-color: #0f172a; - --md-primary-fg-color: #2ab88a; - --md-accent-fg-color: #ff6b6b; -} - -/* Background overlay mimicking the target site gradients */ -body[data-md-color-scheme="default"] { - background-image: - radial-gradient(ellipse at top left, rgba(26, 66, 49, 0.1) 0%, transparent 50%), - radial-gradient(ellipse at bottom right, rgba(139, 38, 53, 0.08) 0%, transparent 50%); - background-attachment: fixed; -} - -/* Typography Overrides */ -.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4, .md-typeset h5, .md-typeset h6 { - font-family: 'Newsreader', serif; - font-weight: 600; - color: #1a4231; -} - -[data-md-color-scheme="slate"] .md-typeset h1, -[data-md-color-scheme="slate"] .md-typeset h2 { - color: #2ab88a; -} - -/* Header Overlay */ -.md-header { - background: rgba(248, 245, 240, 0.8) !important; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(26, 66, 49, 0.1); - color: #1a4231 !important; -} - -[data-md-color-scheme="slate"] .md-header { - background: rgba(11, 15, 25, 0.7) !important; - border-bottom: 1px solid rgba(42, 184, 138, 0.2); - color: #2ab88a !important; -} - -/* Force header links to be visible on the light parchment background */ -.md-header__button.md-logo, -.md-header__button.md-icon, -.md-header__title, -.md-search__input, -.md-tabs__link { - color: #1A1D20 !important; -} - -[data-md-color-scheme="slate"] .md-header__button.md-logo, -[data-md-color-scheme="slate"] .md-header__button.md-icon, -[data-md-color-scheme="slate"] .md-header__title, -[data-md-color-scheme="slate"] .md-tabs__link { - color: #e2e8f0 !important; -} - -/* Code Block Styling */ -.md-typeset pre > code { - border-radius: 8px; - border: 1px solid rgba(0, 0, 0, 0.05); - box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.02); - font-family: 'JetBrains Mono', monospace; -} - -/* Admonition Overrides */ -.md-typeset .admonition { - border-radius: 8px; - background: rgba(0, 0, 0, 0.02); - border: 1px solid rgba(0, 0, 0, 0.05); - border-left: 4px solid var(--md-admonition-fg-color); - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.03); - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.md-typeset .admonition:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); -} - -/* Active Navigation Item */ -.md-nav__item--active > .md-nav__link { - color: #8b2635 !important; - font-weight: 600; -} - -[data-md-color-scheme="slate"] .md-nav__item--active > .md-nav__link { - color: #ff6b6b !important; -} diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 926c1e7..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,128 +0,0 @@ -site_name: NetForge RL -site_description: Multi-Agent Cybersecurity Reinforcement Learning Environment -site_author: Igor Jankowski -site_url: https://reforcemind.github.io/NetForge_RL/ -repo_url: https://github.com/reforcemind/NetForge_RL -repo_name: reforcemind/NetForge_RL -edit_uri: edit/main/docs/ - -theme: - name: material - palette: - - scheme: default - primary: teal - accent: deep orange - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - scheme: slate - primary: teal - accent: deep orange - toggle: - icon: material/brightness-4 - name: Switch to light mode - font: - text: Libre Franklin - code: JetBrains Mono - features: - - navigation.tabs - - navigation.tabs.sticky - - navigation.sections - - navigation.expand - - navigation.top - - navigation.footer - - search.highlight - - search.share - - content.code.copy - - content.code.annotate - - content.tabs.link - icon: - repo: fontawesome/brands/github - logo: material/shield-lock - -extra_css: - - stylesheets/custom.css - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/xaiqo/NetForge_RL - -markdown_extensions: - - admonition - - pymdownx.details - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - pymdownx.tabbed: - alternate_style: true - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - pymdownx.tasklist: - custom_checkbox: true - - attr_list - - md_in_html - - tables - - toc: - permalink: true - -plugins: - - search - - mkdocstrings: - handlers: - python: - paths: [.] - options: - docstring_style: google - show_source: true - show_root_heading: true - show_root_full_path: false - members_order: source - separate_signature: true - show_signature_annotations: true - -nav: - - Home: index.md - - Quick Start: quickstart.md - - Datasheet: DATASHEET.md - - Environment: - - Difficulty & Splits: environment/difficulty.md - - Reproducibility: environment/reproducibility.md - - Graph Observations: environment/graph_observations.md - - Diagnostics: diagnostics/overview.md - - Interoperability: - - SOC Export (OCSF): interop/soc_export.md - - Architecture: - - Overview: architecture/overview.md - - Dynamic Topologies: architecture/dynamic_topology.md - - OT/SCADA Physics: architecture/ot_physics.md - - Multi-Modal Observations: architecture/multimodal_obs.md - - Zero-Trust Identity: architecture/zero_trust.md - - Sim2Real Bridge: architecture/sim2real.md - - NLP-SIEM Pipeline: architecture/nlp_siem.md - - Cybersecurity Reference: - - Environment Overview: cybersec/overview.md - - Threat Model & ATT&CK: cybersec/threat_model.md - - Red Team Actions: cybersec/red_actions.md - - Blue Team Actions: cybersec/blue_actions.md - - API Reference: - - Environment: api/environment.md - - Sim2Real: api/sim2real.md - - SIEM: api/siem.md - - NLP Encoder: api/nlp.md - - Training Guide: - - Curriculum Learning: training/curriculum.md - - RLlib + LSTM: training/rllib.md - - Scenarios: training/scenarios.md - - Benchmarks: - - Overview: benchmarks/overview.md - - Baselines: benchmarks/baselines.md - - Self-Play & Elo: benchmarks/self_play.md - - Run: benchmarks/run.md - - Changelog: changelog.md From 3630b09a325b2674da5b8ce42bc5e8357bf6bf08 Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:15:48 +0200 Subject: [PATCH 2/8] Rewrite existing docs around the cybersecurity RL gym, not a trainer kit. Lead with SIEM vs oracle, PettingZoo/Gymnasium ids, and leave MAPPO/QMIX out of tree. Co-authored-by: Cursor --- DATASHEET.md | 82 +++++++++ docs/DATASHEET.md | 84 ++------- docs/api/environment.md | 13 +- docs/api/nlp.md | 8 +- docs/api/siem.md | 6 +- docs/api/sim2real.md | 6 +- docs/architecture/nlp_siem.md | 13 +- docs/architecture/overview.md | 57 ++---- docs/architecture/sim2real.md | 10 +- docs/architecture/zero_trust.md | 10 +- docs/changelog.md | 34 +++- docs/cybersec/blue_actions.md | 69 ++------ docs/cybersec/overview.md | 22 +-- docs/cybersec/red_actions.md | 70 ++------ docs/cybersec/threat_model.md | 38 ++-- docs/index.md | 305 +++++++++++++++++++++----------- docs/training/rllib.md | 57 +----- docs/training/scenarios.md | 116 +++--------- 18 files changed, 472 insertions(+), 528 deletions(-) create mode 100644 DATASHEET.md diff --git a/DATASHEET.md b/DATASHEET.md new file mode 100644 index 0000000..681a210 --- /dev/null +++ b/DATASHEET.md @@ -0,0 +1,82 @@ +# Datasheet & Responsible Use — NetForge RL + +Following *Datasheets for Datasets* (Gebru et al.) adapted for a simulated +benchmark environment. This accompanies a NeurIPS Datasets & Benchmarks submission. + +## Motivation + +- **Purpose.** NetForge is a research gym and benchmark for autonomous cyber + defense: train, evaluate, break, and compare Red/Blue agents. The simulator + is the engine; NetForge Arena is the standardized evaluation contract + (scenarios, seeds, hidden tests, capability probes, agent cards). +- **Gap addressed.** Existing cyber-RL environments are largely single-agent or + fixed-topology, and they rarely freeze a benchmark protocol. NetForge provides + procedural topologies, belief-state graph observations, a train/dev/hidden + split, scenario packs, risk-sensitive metrics, and a verified JAX throughput + backend. + +## Composition + +- **Instances.** Episodes are procedurally generated network topologies (100 hosts, + 3–5 subnets, optional OT subnet) with per-host OS/CVE/service/credential profiles. +- **Generation.** `NetworkGenerator(seed)` is deterministic per seed. Training draws + from arbitrary seeds; evaluation (`evaluation_mode=True`) draws from a disjoint + held-out pool (seed offset 1000) never seen during training. +- **No personal data.** All hosts, IPs, credentials, and logs are synthetic. CVE + identifiers (e.g. MS17-010, CVE-2019-0708) reference public catalog entries used + only as abstract vulnerability labels; no exploit code is included. + +## Collection / Construction + +- **Dynamics.** Two backends share one action taxonomy. The full-fidelity PettingZoo + env (`environment/parallel_env.py`) carries action durations, an event queue, SIEM + telemetry, and command-list deltas. The vectorized JAX kernel + (`backends/jax/vector_env.py`) implements a reduced transition core and is verified + equivalent to its NumPy reference (`backends/reference.py`) by a trajectory-level + parity test that gates CI (`tests/parity/test_jax_reference_equivalence.py`). The + JAX kernel is a scalable subset, not a bit-equivalent replica of the full env. +- **Rewards.** Per-scenario decompositions; see `benchmarks/env_spec.py` for the full + table. Per-step rewards are small; the OT-safety scenarios (`ot_stuxnet`, and the + kinetic branch of `ransomware`) add a large ±10,000 terminal signal when a PLC is + physically destroyed, so catastrophic physical outcomes dominate the return. + `BaseScenario.normalized_reward` exposes a `tanh`-squashed value in `[-1, 1]` for + cross-scenario comparison. +- **Determinism.** Deterministic under a seed. A per-episode RNG on `GlobalNetworkState` + drives stochastic actions (exploit rolls, phishing, PLC overload); the SIEM logger, + event templates, topology engine, physics, and green agent are all reseeded on + `reset(seed)`. See `tests/parity/test_golden_trajectory.py`. + +## Recommended Uses + +- Benchmarking cooperative/competitive MARL algorithms under partial observability. +- Evaluating LLM agents as SOC operators (`benchmarks/llm_eval.py`). +- Studying generalization via the train/held-out topology split + (`benchmarks/run_benchmark.py --gap`). + +## Limitations + +- **It is a simulation.** Dynamics are abstracted and have **not** been validated + against real network telemetry or live red-team engagements. Results do not + transfer to operational systems without further study. +- **Abstracted exploits.** Exploit success is modeled via CVSS-weighted probabilities + and vulnerability flags, not real exploitation. CVE labels are not a fidelity claim. +- **No real C2 integration.** Live-fire orchestration against real frameworks is + intentionally out of scope and gated behind an unimplemented isolation policy. + +## Responsible Use (dual-use statement) + +This environment trains both attacker and defender policies. It is released for +**defensive research and benchmarking**. The simulator contains no operational +exploit code, no malware, and no capability to act on real systems. Trained Red +policies operate only inside the simulator's abstract action space and confer no +real-world offensive capability. Users must not attempt to connect the environment +to real or production infrastructure. We encourage use aligned with coordinated +vulnerability disclosure norms. + +## Maintenance + +- **Versioning.** Environment changes that alter dynamics are reflected in the + parity reference and gated by CI; the golden-trajectory fingerprint test detects + unintended reward/termination drift. +- **Reproducibility.** Pin the package version and seeds; `benchmarks/` scripts emit + timestamped JSON results with mean ± CI95. diff --git a/docs/DATASHEET.md b/docs/DATASHEET.md index b3ad620..1822975 100644 --- a/docs/DATASHEET.md +++ b/docs/DATASHEET.md @@ -1,65 +1,19 @@ -# Technical Datasheet - -## Environment Composition -- **Topology Size**: 100 host array slots. Active topologies range from 15-30 nodes. Unused nodes use `169.254.0.0/16` addresses, serving as dynamic decoys. -- **State Representation**: `GlobalNetworkState` contains host attributes, firewall ACLs, topologies, and visibility masks. Categorical properties are encoded against static codebooks (`netforge_rl/core/functional.py`). -- **Scenarios**: `ransomware`, `apt_espionage`, `cloud_hybrid`, `iot_grid`, `ot_stuxnet`. - -## Observation & Action Spaces -- **Agents**: `red_operator`, `blue_dmz`, `blue_internal`, `blue_restricted` (PettingZoo parallel). -- **Action space**: `MultiDiscrete([32, 100])` = `[action_type_id, target_host_index]`, with a - 132-d binary action mask (32 action types + 100 targets). -- **Observation** (per-agent `Dict`): `obs` (256-d), `siem_embedding` (128-d), `action_mask` - (132-d), `adj_matrix`, `delta_t`; Blue agents also receive `blue_comm` (100-d shared channel). -- The full, machine-readable spec is generated by `python -m benchmarks.env_spec --json`. - -## Difficulty & Splits -- **Presets**: `easy` / `medium` / `hard` via `netforge_rl.environment.presets`, varying - network size, DHCP churn, dynamic-topology rates, and SIEM latency. See the docs page on - *Difficulty & Splits*. -- **Held-out split**: `evaluation_mode=True` draws from a disjoint seed pool (offset 1000); - `EVAL_SEEDS` is a frozen 20-seed evaluation suite. - -## Metrics and Reward Signals -- Reward signals are defined per-scenario via `BaseScenario`; rewards are `tanh`-normalized. - Per-scenario reward decompositions are published in `env_spec` and on each scenario's - `REWARD_WEIGHTS`. -- Per-episode metrics: MTTC, SLA uptime, total exfiltration, detection rate, and active-host - compromise/isolation counts (padding excluded). - -## Data Generation -- **Source**: `netforge_rl/topologies/network_generator.py`. -- **Determinism**: Byte-identical replay under seed — observations, SIEM embeddings, infos, and - rewards. Every stochastic source (topology, actions, exploit hypervisor, SIEM logger and - templates, green-agent noise) is reseeded on `reset(seed)`; timestamps use a fixed epoch, not - wall-clock. See the *Reproducibility* docs page. -- **Splits**: `evaluation_mode=True` draws from a disjoint held-out seed pool never seen during - training. -- **Limitations**: Synthetic generation; no real network captures. - -## Diagnostics -- Six capability probes (memory, attention, temporal, precision, safety, generalization) that - score a policy in `[0, 1]` on isolated skills, plus an oracle information-asymmetry measure - and a `capability_card` aggregator (JSON + radar chart). - -## Research Features -- **Graph observations**: node/edge/edge-attr arrays (`core/graph_obs.py`) with fog-of-war - masking, mapping directly to PyTorch Geometric / jraph. -- **Deception**: decoys + honeytokens with `deception_hits` / `deception_efficacy` metrics. -- **SOC export**: full-episode SIEM stream to OCSF-style JSON (`siem/export.py`, - `record_siem=True`). -- **Self-play**: population Elo tournament over red/blue policy pools - (`benchmarks/self_play.py`). -- **JAX telemetry**: an in-kernel numeric SIEM signal for the vectorized backend - (`JaxMARLEnv(telemetry_obs=True)`). -- **Trained baselines**: JIT-fused JAX IPPO trainer with committed learning curves + - checkpoints (`benchmarks/train_curve.py`, `jax_ppo.save_params` / `load_params`). -- **Gymnasium single-agent env**: `NetForgeSingleAgentEnv` (passes `check_env`) for - Stable-Baselines3 / CleanRL. -- **ATT&CK coverage**: red actions map to MITRE ATT&CK techniques; `attack_coverage` reported - per episode (`actions/attack_map.py`). - -## Supported Use Cases -- MARL policy training (PPO, IPPO, MAPPO/R-MAPPO) via PettingZoo, RLlib, JaxMARL, CleanRL bridges. -- LLM zero-shot SOC evaluation and PEFT fine-tuning. -- Capability diagnosis and generalization studies. +# Datasheet + +| | | +|---|---| +| Slots | 100 hosts; 15–30 live; padding `169.254.0.0/16` (decoys) | +| State | `GlobalNetworkState` + codebooks in `core/functional.py` | +| Families | `ransomware`, `apt_espionage`, `cloud_hybrid`, `iot_grid`, `ot_stuxnet` | +| Agents | `red_operator`, `blue_dmz`, `blue_internal`, `blue_restricted` | +| Action | `MultiDiscrete([32, 100])`, mask `int8[132]` | +| Obs | `obs` 256, `siem_embedding` 128, `action_mask` 132, `adj_matrix`, `delta_t`; Blue `blue_comm` 100 | +| Spec | `python -m benchmarks.env_spec --json` | +| Difficulty | `easy` / `medium` / `hard`; `evaluation_mode` seed offset 1000; `EVAL_SEEDS` | +| Reward | per-scenario `REWARD_WEIGHTS`, `tanh`-normalized | +| Metrics | MTTC, SLA, exfil, detection, compromise/isolation (padding excluded) | +| Replay | seeded obs / SIEM / infos / rewards; fixed epoch, not wall-clock | +| Limits | synthetic; no real captures | + +Also: belief graphs, decoys, OCSF export, Elo self-play, JAX telemetry, IPPO +checkpoints, `NetForge/Blue-v4`, ATT&CK `attack_coverage`. diff --git a/docs/api/environment.md b/docs/api/environment.md index 9583e0b..d87752f 100644 --- a/docs/api/environment.md +++ b/docs/api/environment.md @@ -1,5 +1,12 @@ # Environment API -::: netforge_rl.environment.parallel_env -::: netforge_rl.backends.jax -::: netforge_rl.core.functional \ No newline at end of file +```{eval-rst} +.. automodule:: netforge_rl.environment.parallel_env + :members: NetForgeRLEnv + :no-undoc-members: + :show-inheritance: + +.. automodule:: netforge_rl.environment.registry + :members: + :no-undoc-members: +``` diff --git a/docs/api/nlp.md b/docs/api/nlp.md index 043a7fd..cfd06a5 100644 --- a/docs/api/nlp.md +++ b/docs/api/nlp.md @@ -1,3 +1,7 @@ -# NLP Encoder API +# NLP encoder API -::: netforge_rl.nlp.log_encoder \ No newline at end of file +```{eval-rst} +.. automodule:: netforge_rl.nlp.log_encoder + :members: + :no-undoc-members: +``` diff --git a/docs/api/siem.md b/docs/api/siem.md index 07b43d9..3cf066d 100644 --- a/docs/api/siem.md +++ b/docs/api/siem.md @@ -1,3 +1,7 @@ # SIEM API -::: netforge_rl.siem.siem_logger \ No newline at end of file +```{eval-rst} +.. automodule:: netforge_rl.siem.siem_logger + :members: + :no-undoc-members: +``` diff --git a/docs/api/sim2real.md b/docs/api/sim2real.md index 8bd0588..05a7b83 100644 --- a/docs/api/sim2real.md +++ b/docs/api/sim2real.md @@ -1,3 +1,7 @@ # Sim2Real API -::: netforge_rl.docker_bridge.bridge \ No newline at end of file +```{eval-rst} +.. automodule:: netforge_rl.docker_bridge.bridge + :members: + :no-undoc-members: +``` diff --git a/docs/architecture/nlp_siem.md b/docs/architecture/nlp_siem.md index 417cdd7..9526c7b 100644 --- a/docs/architecture/nlp_siem.md +++ b/docs/architecture/nlp_siem.md @@ -1,10 +1,5 @@ -# NLP-SIEM Pipeline +# NLP-SIEM -The NLP-SIEM Pipeline translates raw network telemetry strings into vectorized observation spaces or natural language prompts specifically for Blue agents. - -## Mechanics - -- **Event Generation**: The `ConflictResolutionEngine` processes successful and failed state modifications, emitting raw text signatures matching Sysmon/Windows Event Logs (e.g., Event ID 4624 for `Logon`). -- **Log Buffer**: These raw strings are appended to a rolling global event queue (`siem_log_buffer`), which simulates the real-time logging pipeline. -- **Vectorization (RL Agents)**: For standard neural architectures, the most recent $N$ logs in the buffer are mapped through a static TF-IDF encoder or dense embedding layer, transforming the strings into a fixed-size `BlueAgentObservationVector`. -- **Text Translation (LLM Agents)**: For foundation models, the `state_to_text` function directly pulls raw text from the buffer and injects it into a structured natural language prompt, bypassing the vectorization phase entirely. \ No newline at end of file +Conflict resolution emits Sysmon-like strings into `siem_log_buffer`. +RL agents encode the last N logs (TF-IDF or dense). LLM agents get +`state_to_text` from the same buffer. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index f5dcc7c..1c02b52 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,7 +1,9 @@ -# Execution Architecture +# Tick loop -## 1. Simulation Loop -The simulation loop is fully parallelized via the PettingZoo API (`parallel_env.step()`) and JAX vectorization (`jax.vmap`). +Each step: agents pick `[action_type, host_index]`, actions run, conflicts +resolve, SIEM logs fire, Blue obs update from the buffer. + +PettingZoo `parallel_env.step()`. JAX `vmap` is a throughput surrogate. ```mermaid sequenceDiagram @@ -16,48 +18,21 @@ sequenceDiagram Environment->>Environment: action.execute(GlobalNetworkState) Environment->>ConflictResolutionEngine: resolve(effects_dict) ConflictResolutionEngine->>GlobalNetworkState: Apply ActionEffect.state_deltas - Environment-->>Agents: Return new observations, rewards, terminations + Environment-->>Agents: obs, rewards, dones ``` -## 2. Telemetry Pipeline -Blue agent observations are generated exclusively through the simulated Security Information and Event Management (SIEM) pipeline. +Blue obs come from SIEM only. ```mermaid flowchart TD - subgraph Action Execution - R1[Red Actions] - end - - subgraph Simulation Core - GNS[(GlobalNetworkState)] - CRE{Conflict Resolution Engine} - SL[SIEMLogger] - end - - subgraph Blue Observability - LB[(siem_log_buffer)] - NLP[NLP Log Encoder] - BO[Blue Agent Observation Vector] - end - - R1 -->|ActionEffect| CRE - CRE -->|Valid Effects| GNS - CRE -->|Action Metrics| SL - - SL -.->|Stochastic Noise Injection| SL - SL -->|Generates Sysmon/Windows Logs| LB - - LB -->|N Most Recent Logs| NLP - NLP -->|TF-IDF / Dense Embeddings| BO + R1[Red Actions] -->|ActionEffect| CRE{Conflict Resolution} + CRE -->|Valid Effects| GNS[(GlobalNetworkState)] + CRE -->|Metrics| SL[SIEMLogger] + SL -.->|Noise| SL + SL -->|Sysmon-like logs| LB[(siem_log_buffer)] + LB --> NLP[Log Encoder] + NLP --> BO[Blue observation] ``` -## 3. Component Details - -### `BaseAction` and `ActionEffect` -All agent capabilities inherit from `BaseAction`. `execute()` returns an `ActionEffect` containing `state_deltas` determining specific state tensor modifications. - -### `ConflictResolutionEngine` -Resolves temporal collisions occurring in the same parallel tick. Handled deterministically within the `EnvState` interpreter. - -### `SIEMLogger` and `LogEncoder` -`SIEMLogger` translates state deltas into standardized string logs matching Windows/Sysmon syntax, injecting stochastic benign noise. When `log_latency > 0` it holds each log for that many ticks before it becomes visible, modelling a lagging SOC feed. `LogEncoder` vectorizes the buffer into dense observations. +`BaseAction.execute()` returns `ActionEffect.state_deltas`. +Resolution is deterministic per tick. `log_latency` can delay logs. diff --git a/docs/architecture/sim2real.md b/docs/architecture/sim2real.md index b7d2075..5a6c8d1 100644 --- a/docs/architecture/sim2real.md +++ b/docs/architecture/sim2real.md @@ -1,9 +1,5 @@ # Sim2Real Bridge -The Sim2Real Bridge interfaces internal RL action tensors into executable network commands for live environments, such as Docker containers (e.g., Vulhub). - -## Mechanics - -- **Translation Mapping**: Maps discrete RL action integers (e.g., `ActionType.ExploitEternalBlue`) to standardized payload sequences (e.g., Metasploit RPC commands). -- **Execution Engine**: Interacts with the live target via a strictly typed execution wrapper. It captures the physical stdout/stderr output resulting from the payload injection. -- **State Feedback**: Evaluates the output against expected success signatures. If validation passes, the corresponding simulated `GlobalNetworkState` tensor is updated to reflect the physical reality (e.g., escalating privilege to `Root`). \ No newline at end of file +Maps discrete actions (e.g. `ExploitEternalBlue`) to live commands (Docker / +Vulhub). Stdout/stderr is matched against success signatures; on hit, the +sim `GlobalNetworkState` is updated (e.g. privilege → Root). diff --git a/docs/architecture/zero_trust.md b/docs/architecture/zero_trust.md index b2ede4e..bffc6d1 100644 --- a/docs/architecture/zero_trust.md +++ b/docs/architecture/zero_trust.md @@ -1,9 +1,5 @@ # Zero-Trust Identity -The Zero-Trust Identity module governs authorization tokens and their alignment with `GlobalNetworkState` access vectors. - -## Mechanics - -- **Token Arrays**: Each active node possesses a continuous token array representing authorized privilege contexts. -- **Verification**: Red agents executing identity-based lateral movement (e.g., `PassTheHash`, `PassTheTicket`) are validated strictly against this array rather than service vulnerability flags. -- **Invalidation**: Blue agents execute actions like `RotateKerberos` to forcefully clear token arrays globally. If a Red agent attempts to utilize an invalidated token for authentication, the action `success` flag returns `False` and a failed authentication event is passed to the SIEM buffer. \ No newline at end of file +Each node holds a token array. Identity lateral movement (`PassTheHash`, +`PassTheTicket`) checks tokens, not CVEs. Blue `RotateKerberos` clears them; +stale tokens fail and log to SIEM. diff --git a/docs/changelog.md b/docs/changelog.md index adb0c78..958819f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,40 @@ # Changelog -All notable changes to the `netforge_rl` project will be documented in this file. +All notable changes to the `netforge-rl` project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [4.0.0] — 2026-09-20 + +Cybersecurity RL gym: PettingZoo / Gymnasium ids, SIEM belief graphs, eval CLI. +Not on PyPI. + +### Added +- **NetForge Arena 2026**: frozen train/dev/hidden splits, competition metrics + (mission, disruption, security, false positives, exfiltration, catastrophic, + CVaR / worst-case), OOD tasks, adversarial Red populations, leaderboard helpers. +- **`netforge` CLI**: `run`, `test-policy`, `benchmark`, `arena`, `replay`, `collect`, + `questions`, `evaluate`. + Package name `netforge-rl`. +- **Belief-state graphs**: Blue `info['graph']` is reconstructed from SIEM + inventory; + privileged `oracle_graph` is diagnostic/replay only. +- **Capability probes**: deception resistance and adaptation (mid-episode Red shift), + plus agent cards (`Safety`, `OOD Generalization`, `Memory`, `False Positive Rate`, …). +- **Scenario YAML / community packs**: hospital ransomware, cloud IAM, enterprise APT, + finance, IoT, OT, zero-trust. `netforge run my_scenario.yaml`. +- **Reward-design and constrained RL wrappers**; communication-failure wrapper; + league / PSRO payoff helper; event vs fixed `time_mode`. +- **Offline datasets**: trajectory collector + Minari-compatible npz / HDF5 export. +- **Named probes** (`netforge questions`) and CAGE-style submissions + (`netforge evaluate`) ranked on Arena metrics, not mean reward. +- **Gymnasium / PettingZoo registration**: `NetForge/Blue-v4`, `netforge/ransomware-v4`. +- **Interactive HTML replay viewer** (network + SIEM + reward timeline). +- **Sphinx + Furo** docs (MyST), GitHub Pages from Actions — same kit as FlowEdge. + +### Changed +- README and docs lead with Red vs Blue and SIEM. MAPPO / QMIX / CT-GMARL are + evaluated *on* the gym, not vendored into it. +- Blue shared comm no longer leaks ground-truth privilege. + ## [3.1.0] — 2026-07-10 ### Fixed diff --git a/docs/cybersec/blue_actions.md b/docs/cybersec/blue_actions.md index 6208c17..cfa378a 100644 --- a/docs/cybersec/blue_actions.md +++ b/docs/cybersec/blue_actions.md @@ -1,53 +1,16 @@ -# Blue Team Actions - -### `ConfigureACL` -- **Module:** `mitigation.py` -- **Description:** Modifies the firewall state to drop traffic targeting specific ports on a subnet. - -### `IsolateHost` -- **Module:** `mitigation.py` -- **Description:** Sets target host status to isolated, dropping all inbound and outbound traffic capabilities. - -### `Remove` -- **Module:** `mitigation.py` -- **Description:** Resets the compromised state of a target host, terminating executing Red policies on that node. - -### `RestoreFromBackup` -- **Module:** `mitigation.py` -- **Description:** Reverts target host state vector to baseline initial configuration. - -### `RestoreHost` -- **Module:** `mitigation.py` -- **Description:** Reverts host status from isolated, re-establishing network connectivity. - -### `SecurityAwarenessTraining` -- **Module:** `mitigation.py` -- **Description:** Decreases the `human_vulnerability_score` scalar for all nodes in a target subnet. - -### `DecoyApache` / `DecoySSHD` / `DecoyTomcat` -- **Module:** `deception.py` -- **Description:** Instantiates a decoy service node matching specific port signatures (80, 22, 8080) to intercept discovery requests. - -### `DeployDecoy` -- **Module:** `deception.py` -- **Description:** Instantiates a generic decoy service node. - -### `DeployHoneytoken` -- **Module:** `deception.py` -- **Description:** Injects a honeytoken state into a host. Subsequent token extraction actions by Red agents trigger an explicit SIEM event with perfect confidence mapping to the executor's ID. - -### `Misinform` -- **Module:** `deception.py` -- **Description:** Alters telemetry states to return falsified observation arrays to Red agent discovery actions. - -### `RotateKerberos` -- **Module:** `identity.py` -- **Description:** Invalidates existing Domain Controller token arrays globally. - -### `Analyze` -- **Module:** `analysis.py` -- **Description:** Queries the complete unmasked state of a specific host node. - -### `Monitor` -- **Module:** `analysis.py` -- **Description:** Increases the SIEM event generation probability scalar for actions executing on a target subnet. +# Blue actions + +| Action | Module | | +|---|---|---| +| `ConfigureACL` | mitigation | drop ports on a subnet | +| `IsolateHost` | mitigation | cut all traffic | +| `RestoreHost` | mitigation | un-isolate | +| `Remove` | mitigation | clear compromise | +| `RestoreFromBackup` | mitigation | reset to baseline | +| `SecurityAwarenessTraining` | mitigation | lower `human_vulnerability_score` | +| `DecoyApache` / `SSHD` / `Tomcat` | deception | sinkhole discovery | +| `DeployDecoy` / `DeployHoneytoken` | deception | decoy node / token → SIEM on use | +| `Misinform` | deception | lie on Red discovery | +| `RotateKerberos` | identity | wipe DC tokens | +| `Analyze` | analysis | unmasked host (diagnostic) | +| `Monitor` | analysis | raise SIEM rate on a subnet | diff --git a/docs/cybersec/overview.md b/docs/cybersec/overview.md index 156a313..b56b8bc 100644 --- a/docs/cybersec/overview.md +++ b/docs/cybersec/overview.md @@ -1,19 +1,11 @@ -# Environment Overview +# Cyber -NetForge RL defines a zero-sum, asymmetric, multi-agent environment with partial observability. +Asymmetric POMDP on a simulated network. -## Red Policy Characteristics +**Red** starts with no visibility: discover, exploit, escalate, exfil, or hit a +PLC. Success needs the right CVE / token / routing, not a dice roll. -- **Objective**: Maximize scalar reward by successfully executing impact actions (e.g., data exfiltration, service disruption) on designated target nodes. -- **Initial State**: Zero network visibility. `GlobalNetworkState` is fully masked. -- **Mechanics**: Agents must sequentially execute discovery, exploit, and privilege escalation actions to modify node states. Success probabilities are calculated deterministically against the target's vulnerability array and current access level. +**Blue** sees SIEM, not the true compromise map. Isolate, ACL, restore, decoys +(`169.254.x.x`). Quiet Red may never show up in logs. -## Blue Policy Characteristics - -- **Objective**: Maximize scalar reward by maintaining service uptime and minimizing compromised host counts across the episode. -- **Initial State**: Full visibility of the uncompromised network baseline. -- **Mechanics**: Agents observe the environment through SIEM event logs. Actions consist of isolation, firewall ACL modification, host restoration, and decoy deployment (`169.254.x.x`). Detection capabilities depend on Red action signatures; stealthy actions may bypass event generation. - -## Kinetic Impacts - -The environment supports Cyber-Physical System (CPS) nodes. Specific Red actions (`OverloadPLC`) can transition CPS nodes into a `kinetic_destruction` state. This represents a terminal fail-state for Blue agents, immediately ending the episode. \ No newline at end of file +**Kinetic** — `OverloadPLC` can set `kinetic_destruction` (Blue fail-state). diff --git a/docs/cybersec/red_actions.md b/docs/cybersec/red_actions.md index 10d9c0c..7d2374d 100644 --- a/docs/cybersec/red_actions.md +++ b/docs/cybersec/red_actions.md @@ -1,54 +1,16 @@ -# Red Team Actions - -### `DumpLSASS` -- **Module:** `post_exploitation.py` -- **Description:** Extracts token arrays from memory. Requires `Root` privilege state on the target node. - -### `ExploitBlueKeep` / `ExploitEternalBlue` / `ExploitHTTP_RFI` -- **Module:** `exploits.py` -- **Description:** Target specific service vulnerabilities (RDP, SMB, HTTP). Validates against host vulnerability vectors to modify access state. - -### `ExploitRemoteService` -- **Module:** `exploits.py` -- **Description:** Generic exploit attempting to modify access state from network visibility to local `User` privilege. -- **Args:** `agent_id` (str), `target_ip` (str), `port` (int). - -### `PassTheTicket` / `PassTheHash` -- **Module:** `post_exploitation.py` / `privilege_escalation.py` -- **Description:** Validates previously extracted token arrays to modify access state on target nodes without requiring vulnerable services. - -### `PrivilegeEscalate` -- **Module:** `privilege_escalation.py` -- **Description:** Modifies agent privilege state from `User` to `Root`/`SYSTEM`. -- **Args:** `agent_id` (str), `target_ip` (str). - -### `DiscoverNetworkServices` -- **Module:** `reconnaissance.py` -- **Description:** Queries the active service array of a target node. Updates agent visibility mask. -- **Args:** `agent_id` (str), `target_ip` (str). - -### `DiscoverRemoteSystems` / `NetworkScan` -- **Module:** `reconnaissance.py` -- **Description:** Queries the active IP allocation array of a target subnet. Updates agent visibility mask. -- **Args:** `agent_id` (str), `target_subnet` (str). - -### `ShareIntelligence` -- **Module:** `coordination.py` -- **Description:** Performs a union operation on the visibility masks of two allied agents. - -### `ExfiltrateData` / `Impact` / `KillProcess` -- **Module:** `impact.py` -- **Description:** Executes terminal objectives. Modifies the data state or process array on a compromised node to calculate scenario rewards. - -### `JuicyPotato` / `V4L2KernelExploit` -- **Module:** `privilege_escalation.py` -- **Description:** Platform-specific privilege escalation actions targeting specific OS vectors (Windows DCOM / Linux Kernel). - -### `OverloadPLC` -- **Module:** `kinetic.py` -- **Description:** Modifies the physical state tensor of an OT node to `kinetic_destruction`. - -### `SpearPhishing` -- **Module:** `social_engineering.py` -- **Description:** Evaluates success strictly against the `human_vulnerability_score` scalar of a target endpoint. Bypasses routing graph constraints. -- **Args:** `agent_id` (str), `target_ip` (str). +# Red actions + +| Action | Module | | +|---|---|---| +| `DiscoverRemoteSystems` / `NetworkScan` | reconnaissance | subnet IPs → visibility | +| `DiscoverNetworkServices` | reconnaissance | services on a host | +| `ExploitBlueKeep` / `EternalBlue` / `HTTP_RFI` | exploits | RDP / SMB / HTTP vs CVE flags | +| `ExploitRemoteService` | exploits | network vis → User | +| `PrivilegeEscalate` | privilege_escalation | User → Root | +| `JuicyPotato` / `V4L2KernelExploit` | privilege_escalation | Windows DCOM / Linux kernel | +| `DumpLSASS` | post_exploitation | tokens; needs Root | +| `PassTheHash` / `PassTheTicket` | post_exploitation | tokens, not CVEs | +| `ShareIntelligence` | coordination | union of visibility masks | +| `ExfiltrateData` / `Impact` / `KillProcess` | impact | terminal objectives | +| `OverloadPLC` | kinetic | PLC → `kinetic_destruction` | +| `SpearPhishing` | social_engineering | vs `human_vulnerability_score`; no routing | diff --git a/docs/cybersec/threat_model.md b/docs/cybersec/threat_model.md index 221ef7a..52f65f7 100644 --- a/docs/cybersec/threat_model.md +++ b/docs/cybersec/threat_model.md @@ -1,29 +1,15 @@ -# Action Taxonomy Mapping +# Threat model -NetForge RL actions are mapped to specific execution mechanics rather than abstract probabilities. +Actions are mechanics, not dice rolls. -## Implemented Vectors +| | | +|---|---| +| Spearphish | `human_vulnerability_score`; no routing | +| BlueKeep / EternalBlue / HTTP RFI | RDP / SMB / HTTP CVE flags | +| JuicyPotato / V4L2 | DCOM / Linux kernel flags | +| Pass-the-hash | DC tokens, not CVEs | +| Honeytoken | unmaskable SIEM on ingest | +| Decoys | Apache / Tomcat / SSH sinkholes | -### Initial Access & Execution -- **Spearphishing**: Evaluated against the `human_vulnerability_score` scalar of the target end-user node. Bypasses routing constraints. -- **Remote Exploits**: - - `ExploitBlueKeep` (CVE-2019-0708): Modifies access state on RDP. - - `ExploitEternalBlue` (MS17-010): Modifies access state on SMB. - - `ExploitHTTP_RFI`: Modifies access state on HTTP/HTTPS. - -### Privilege Escalation -Agents transitioning from `User` to `Root`/`SYSTEM` state requirements: -- **JuicyPotato**: Exploits DCOM (`SeImpersonatePrivilege`) on Windows nodes. -- **V4L2 Kernel Exploit**: Targets specific kernel array flags on Linux nodes. - -### Lateral Movement -- **Pass the Hash**: Validates token exchanges against Domain Controller state vectors, allowing state modification without exploiting service CVEs. - -### Defense Evasion & Deception -Blue agent countermeasures: -- **DeployHoneytoken**: Injects a monitored token state into the target host. Ingestion by a Red agent generates an unmaskable SIEM event alert. -- **Decoys**: Instantiates isolated nodes mimicking Apache/Tomcat/SSH services to intercept and sinkhole Red discovery actions. - -## Visibility Masks -- **Red Policy**: Local knowledge graph updated only via successful discovery actions (`NetworkScan`, `DiscoverRemoteSystems`). -- **Blue Policy**: Graph updated only when an executed action triggers a SIEM log event according to its predefined signature. \ No newline at end of file +Red visibility updates only on successful discovery. Blue visibility updates +only when an action emits a SIEM signature. diff --git a/docs/index.md b/docs/index.md index 32bcac6..e316fea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,112 +1,217 @@ -
- Version 3.1.0 - Python 3.12+ - JAX - PettingZoo - MITRE ATT&CK +# NetForge + +

Cybersecurity gym for reinforcement learning. Red compromises hosts. Blue contains them from SIEM, not from an oracle. Train with PettingZoo or Gymnasium.

+ +```{raw} html +
    +
  • Red vs Blue
  • +
  • SIEM telemetry
  • +
  • PettingZoo
  • +
  • Gymnasium
  • +
+ +
+
+ 1 Red · 3 Blue + DMZ, internal, restricted +
+
+ SIEM only + Blue never sees the true map +
+
+ durative actions + exploits and isolates take ticks +
-# NetForge RL - -**A fast, reproducible multi-agent reinforcement learning environment for autonomous -cyber-defense.** Red agents attack a procedurally generated enterprise/OT network -(exploit, pivot, escalate, ransomware, kinetic sabotage); Blue agents defend it (monitor, -analyze, isolate, deceive, restore). Built on the PettingZoo parallel API with a JAX -vectorized backend for high-throughput training. - ---- - -## Why NetForge - -- **Standardized API** — PettingZoo `ParallelEnv`, Gymnasium spaces, conformance-tested. -- **Partial observability that means something** — Blue sees only a filtered, optionally - *delayed* SIEM feed; Red operates under fog of war and must recon before it can exploit. -- **Realistic telemetry** — actions emit real Windows/Sysmon event XML (4624, 4688, Sysmon - 1/3/10/22, …) which an NLP encoder turns into observations. Plug an LLM in as a SOC analyst. -- **MITRE ATT&CK aligned** — exploits map to real CVEs (MS17-010, CVE-2019-0708, Log4Shell). -- **Reproducible to the bit** — a fixed seed replays observations, SIEM embeddings, infos and - rewards identically (test-guaranteed). See [Reproducibility](environment/reproducibility.md). -- **Tunable difficulty** — named `easy`/`medium`/`hard` presets and a frozen held-out - evaluation split. See [Difficulty & Splits](environment/difficulty.md). -- **Diagnostic probes + capability cards** — a 6-capability suite (memory, attention, temporal, - precision, safety, generalization) that isolates *what* a policy can and cannot do, summarised - as a per-policy radar card. See [Diagnostics](diagnostics/overview.md). -- **Graph-native observations** — hosts as nodes, reachability as edges, fog-of-war aware, - one line to PyTorch Geometric / jraph. See [Graph Observations](environment/graph_observations.md). -- **Self-play & Elo** — a population tournament that rates red and blue on one ladder. See - [Self-Play](benchmarks/self_play.md). -- **Deception as a mechanic** — decoys and honeytokens with a `deception_efficacy` metric - measuring how much of Red's effort was wasted on traps. -- **SOC export** — replay an episode as OCSF-style JSON for real SIEM tooling. See - [SOC Export](interop/soc_export.md). -- **Trained baselines, not just scripted** — a JIT-fused JAX IPPO trainer whose whole rollout - runs on-device; a committed `ransomware` run learns mean reward 0.06 → 0.71. Checkpoints - reload into self-play and capability cards. See [Baselines](benchmarks/baselines.md). -- **Standard single-agent API** — `NetForgeSingleAgentEnv` is a `gymnasium.Env` (passes - `check_env`) for Stable-Baselines3 / CleanRL, with the action mask in `info`. -- **MITRE ATT&CK coverage** — episodes report which ATT&CK techniques Red exercised and the - fraction of the taxonomy covered (`attack_coverage`). -- **Fast** — JAX backend measured at **270,795 env-steps/s (1,083,181 agent-steps/s)** at - batch 4096 on CPU, with an optional in-kernel numeric SIEM signal. See - [Benchmarks](benchmarks/overview.md). -- **JAX backend matches real action timing** — the vectorized backend enqueues and resolves - actions against the same per-agent duration table as the Python engine: an agent can't act - again until its pending action resolves, and isolating a host cancels an in-flight exploit - against it. Verified against a NumPy reference with zero mismatches across all five - scenarios under randomized rollouts. - -## Quick Start + +``` + +## News + +**September 2026** + +- Gym ids: PettingZoo `netforge/ransomware-v4`, Gymnasium `NetForge/Blue-v4`. +- `netforge` CLI: `run`, `evaluate`, `questions`, HTML replay. +- Arena train / dev / hidden. Typed `EnvConfig`. {doc}`changelog` + +## The problem + +A defender does not get the true compromise map. The SOC sees Sysmon-like logs +that can be late, dropped, or noisy. Red starts blind. Exploits and isolates +take ticks. A gym that hands Blue an oracle, or scores mean reward against one +scripted Red, is training the wrong job. + +## What NetForge does + +Red starts blind, discovers hosts, exploits CVEs, escalates, exfiltrates or +hits a PLC. Blue isolates, restores, drops decoys, and reads Sysmon-like logs +that can be late or noisy. Padding hosts (`169.254.x.x`) are decoys. + +```{image} _static/figures/purpose.svg +:alt: Red, Blue, and the network +:class: fe-fig +``` + +```{image} _static/figures/workflow.svg +:alt: reset, observe SIEM, act, next tick +:class: fe-fig +``` + +```{image} _static/figures/why-this.svg +:alt: God-mode vs SIEM, instant logs vs delay, isolate-all vs SLA +:class: fe-fig +``` + +## Why this, not the usual stack + +Blue never sees the true map. Logs can lag. Isolate-everything wrecks SLA. +Eval uses a Red population. Trainers stay in your repo. + +## Install ```bash -pip install 'netforge_rl[jax] @ git+https://github.com/reforcemind/NetForge_RL' +python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL' +netforge run hospital_ransomware --replay replay.html ``` ```python -from netforge_rl.environment import make_env +import netforge_rl +import gymnasium as gym +from pettingzoo import make -env = make_env('medium', scenario_type='ransomware', seed=0) +env = make("parallel", "netforge/ransomware-v4", max_ticks=80) obs, infos = env.reset(seed=0) -while env.agents: - actions = {a: env.action_space(a).sample() for a in env.agents} - obs, rewards, term, trunc, infos = env.step(actions) - if all(term.values()) or all(trunc.values()): - break +blue = gym.make("NetForge/Blue-v4", max_ticks=80) +``` + +Not on PyPI. Python 3.12+. Trainers stay in your repo. + +MIT, with CybORG / DSTG notices in the LICENSE file. + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Start +Overview +getting-started +python-quickstart +arena +questions +submissions +capabilities +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Guides +scenarios +baselines +datasets +backends +diagnostics/overview +environment/difficulty +environment/reproducibility +environment/graph_observations +training/curriculum +training/rllib +training/scenarios +guides/verification +interop/pettingzoo +interop/soc_export +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Benchmarks +benchmarks/overview +benchmarks/baselines +benchmarks/self_play +benchmarks/run +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Architecture +architecture/overview +architecture/dynamic_topology +architecture/ot_physics +architecture/multimodal_obs +architecture/zero_trust +architecture/sim2real +architecture/nlp_siem +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Cyber +cybersec/overview +cybersec/threat_model +cybersec/red_actions +cybersec/blue_actions +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: API +api/python +api/environment +api/siem +api/nlp +api/sim2real +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Contribute +contributing +research ``` -See the [Quick Start Guide](quickstart.md) for the raw PettingZoo loop, the JAX vectorized -loop, difficulty presets, baselines, and diagnostics. - -## What's inside - -| Layer | Package | Purpose | -|---|---|---| -| Environment | `environment/` | PettingZoo `NetForgeRLEnv`, difficulty presets, curriculum | -| Scenarios | `scenarios/` | 5 reward/objective families (ransomware, APT, cloud, IoT, OT) | -| Actions | `actions/red`, `actions/blue` | ATT&CK-aligned red/blue capabilities | -| Telemetry | `siem/`, `nlp/` | Sysmon/Windows log synthesis + NLP encoding | -| JAX backend | `backends/jax/` | Vectorized `vmap`/`jit` kernels + NumPy reference | -| Baselines | `baselines/` | Random, heuristic, kill-chain red, JAX PPO | -| Diagnostics | `diagnostics/` | Capability probes + oracle information-asymmetry | -| Bridges | `bridges/` | RLlib, JaxMARL, CleanRL, DLPack adapters | -| Semantic | `semantic/` | LLM SOC agents, grammars, fine-tuning recipes | - -## The five scenarios - -| Scenario | Red objective | Blue objective | Terminal condition | -|---|---|---|---| -| `ransomware` | Encrypt/compromise Corporate + Secure | Contain, restore, avoid downtime | all Corporate/Secure compromised, or PLC kinetic | -| `apt_espionage` | Stealthy persistence + exfiltration | Detect and isolate every foothold | every infected host isolated | -| `cloud_hybrid` | Breach the Secure enclave | Protect the Secure subnet SLA | every Secure host compromised | -| `iot_grid` | Take the grid controllers | Keep controllers healthy | all controllers compromised | -| `ot_stuxnet` | Drive a PLC to kinetic destruction | Prevent physical damage | any PLC kinetic destruction | - -## Citation - -```bibtex -@misc{netforge_rl_2026, - title = {NetForge RL: A Fast Multi-Agent Cybersecurity Benchmark for Reinforcement Learning}, - author = {ReforceMind}, - year = {2026}, - url = {https://github.com/reforcemind/NetForge_RL} -} +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Project +roadmap +product-direction +DATASHEET +changelog ``` diff --git a/docs/training/rllib.md b/docs/training/rllib.md index adef1ad..974edc1 100644 --- a/docs/training/rllib.md +++ b/docs/training/rllib.md @@ -1,8 +1,4 @@ -# Training with Ray RLlib - -## Environment Wrapper - -NetForge implements the RLlib `MultiAgentEnv` interface via `NetForgeRLlibEnv`. +# Ray RLlib ```python from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv @@ -10,17 +6,8 @@ from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv env = NetForgeRLlibEnv({"scenario_type": "ransomware", "max_ticks": 100}) ``` -## RMAPPO Baseline - -The reference training script is [`benchmarks/rllib_rmappo.py`](https://github.com/reforcemind/NetForge_RL/blob/main/benchmarks/rllib_rmappo.py). It trains two shared LSTM policies (one red, one blue) with PPO on the legacy API stack. - -```bash -python benchmarks/rllib_rmappo.py -``` - -Key settings: `train_batch_size=1024`, `use_lstm=True`, legacy API stack (`enable_rl_module_and_learner=False`). Custom metrics from `NetForgeMetricsCallback` are logged per-agent each step. - -## PPO Configuration +Reference: `python benchmarks/rllib_rmappo.py` — shared LSTM PPO, red + blue, +`train_batch_size=1024`, legacy API stack. ```python import ray @@ -30,11 +17,13 @@ from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv ray.init() register_env("netforge-v0", lambda cfg: NetForgeRLlibEnv(cfg)) - config = ( PPOConfig() .environment("netforge-v0", env_config={"scenario_type": "ransomware"}) - .api_stack(enable_rl_module_and_learner=False, enable_env_runner_and_connector_v2=False) + .api_stack( + enable_rl_module_and_learner=False, + enable_env_runner_and_connector_v2=False, + ) .multi_agent( policies={ "red_rmappo": (None, None, None, {"model": {"use_lstm": True}}), @@ -47,36 +36,6 @@ config = ( .training(train_batch_size=1024) ) algo = config.build() -result = algo.train() -``` - -## Curriculum Learning - -`CurriculumWrapper` wraps `NetForgeRLEnv` and automatically progresses through three phases based on rolling mean episode reward. - -| Phase | Active hosts | Scenarios | DHCP | Dynamic topology | Reward scale | Graduate at | -|---|---|---|---|---|---|---| -| `novice` | 5 | ransomware | off | off | 3× | mean ≥ 60 over 10 eps | -| `intermediate` | 25 | ransomware, apt | 80 ticks | off | 1.5× | mean ≥ 40 over 15 eps | -| `expert` | 100 | ransomware, apt, iot | 40 ticks | churn + migration | 1× | final | - -```python -from netforge_rl.environment.curriculum import CurriculumWrapper - -env = CurriculumWrapper( - base_cfg={"max_ticks": 200}, - on_phase_advance=lambda idx, name: print(f"Phase {idx}: {name}"), -) -obs, _ = env.reset(seed=0) ``` -Each step's `info` dict contains `__curriculum__` with `phase`, `phase_index`, `mean_reward`, `window_fill`, and `phase_advanced`. - -Start from a specific phase or supply custom phases: - -```python -from netforge_rl.environment.curriculum import CurriculumWrapper, PHASES - -env = CurriculumWrapper(phases=PHASES, start_phase=1) # start at intermediate -``` -``` \ No newline at end of file +Curriculum: {doc}`curriculum`. diff --git a/docs/training/scenarios.md b/docs/training/scenarios.md index 68c12f3..650b4d2 100644 --- a/docs/training/scenarios.md +++ b/docs/training/scenarios.md @@ -1,123 +1,51 @@ -# Custom Scenarios +# Custom scenarios -Custom scenarios define the **win conditions** and **reward tensors** for the environment. Core resolution logic (e.g., executing an exploit) is handled independently by the `Functional Core`. - -## BaseScenario Interface - -Scenarios must inherit from `netforge_rl.scenarios.base_scenario.BaseScenario` and implement two abstract methods: - -1. `calculate_reward(self, agent_id, global_state, effect)`: Returns a scalar float representing the delta reward for the current step. -2. `check_termination(self, global_state)`: Returns a dictionary mapping agent IDs to boolean terminal states. - -## Configuration: Ransomware - -In a Ransomware configuration, the Red agent seeks to compromise system integrity flags, while the Blue agent seeks to maximize healthy host ratios. - -### 1. Structure and Initialization +Subclass `BaseScenario`: `calculate_reward(agent_id, global_state, effect)` and +`check_termination(global_state)`. Exploits stay in the functional core. ```python from netforge_rl.scenarios.base_scenario import BaseScenario class CustomRansomware(BaseScenario): - def __init__(self, agents): - self.agents = agents -``` - -### 2. Base Reward Routing - -A central `calculate_reward` method routes scalar logic to specific agent functions, applying global penalties (like action cost) universally. - -```python def calculate_reward(self, agent_id, global_state, effect=None): reward = 0.0 - - # Base penalty for taking actions if effect and getattr(effect, 'cost', 0) > 0: reward -= effect.cost * 0.05 - if 'red' in agent_id: return self._red_reward(global_state, effect) return self._blue_reward(global_state, effect) -``` - -### 3. Red Agent Logic -Red agents are rewarded exclusively for successfully modifying the `GlobalNetworkState` access and integrity tensors. - -```python def _red_reward(self, global_state, effect): - reward = 0.0 - if effect and effect.success and effect.state_deltas: - for key, val in effect.state_deltas.items(): - if 'privilege' in key and val == 'Root': - reward += 5.0 - if 'system_integrity' in key and val == 'kinetic_destruction': - reward += 10000.0 - return reward -``` - -### 4. Blue Agent Logic - -Blue agents are rewarded proportionally to the number of nodes maintaining a baseline (healthy) state vector. + if not (effect and effect.success and effect.state_deltas): + return 0.0 + r = 0.0 + for key, val in effect.state_deltas.items(): + if 'privilege' in key and val == 'Root': + r += 5.0 + if 'system_integrity' in key and val == 'kinetic_destruction': + r += 10000.0 + return r -```python def _blue_reward(self, global_state, effect): + hosts = global_state.all_hosts.values() healthy = sum( - 1 for h in global_state.all_hosts.values() + 1 for h in hosts if h.compromised_by == 'None' and h.status != 'isolated' ) return healthy / len(global_state.all_hosts) -``` -### 5. Termination Logic - -The episode halts immediately if a critical Cyber-Physical node transitions to a destroyed state. - -```python def check_termination(self, global_state): - if any(h.system_integrity == 'kinetic_destruction' for h in global_state.all_hosts.values()): - return {agent: True for agent in self.agents} - - return {agent: False for agent in self.agents} -``` - -## OT/SCADA Scenario: `ot_stuxnet` - -The `OTStuxnetScenario` rewards Red for achieving physical process destruction on PLC hosts, and Blue for keeping OT nodes clean. A 25% chance each episode generates an OT subnet (`10.0.99.0/24`) with `PLC_Firmware` hosts. - -### Physical state - -Each PLC host tracks three process variables governed by first-order lag ODEs (in `PLCPhysicsEngine`): - -| Variable | Nominal | Alarm | Critical | τ (ticks) | -|---|---|---|---|---| -| temperature (°C) | 40–60 | > 80 | > 120 | 20 | -| pressure (bar) | 90–110 | > 130 or < 70 | > 180 or < 30 | 10 | -| flow_rate (L/min) | 40–60 | > 90 or < 20 | > 150 or < 5 | 5 | - -Each tick: `x[t+1] = x[t] + (setpoint − x[t]) / τ + noise` - -### Attack flow - -`OverloadPLC` (red_operator action ID 20, requires Root on a PLC host) manipulates setpoints rather than directly spiking values: - -```python -# setpoints jump to unsafe targets; physics engine drives process toward them -temperature_setpoint += uniform(80, 150) # °C above normal -pressure_setpoint *= uniform(1.5, 2.0) # 1.5–2× nominal -flow_rate_setpoint = 3.0 # near-zero (choke) + dead = any( + h.system_integrity == 'kinetic_destruction' + for h in global_state.all_hosts.values() + ) + return {agent: dead for agent in self.agents} ``` -Blue receives `SCADA_PHYSICAL_ALARM` SIEM alerts (severity 7) when variables enter alarm range, and `SCADA_KINETIC_BREACH` (severity 10) when critical thresholds are crossed. Blue has roughly 10 ticks between first alarm and physical destruction to isolate the PLC. +`ot_stuxnet`: PLC ODE physics, `OverloadPLC` moves setpoints, Blue has ~10 ticks +after `SCADA_PHYSICAL_ALARM` to isolate. Tables: {doc}`../architecture/ot_physics`. ```python env = NetForgeRLEnv({'scenario_type': 'ot_stuxnet', 'max_ticks': 300}) -``` - -## Registering Configurations - -Scenario classes are registered dynamically via the `parallel_env` instantiation dictionary: - -```python env = NetForgeRLEnv({'scenario_type': 'custom_ransomware'}) -``` \ No newline at end of file +``` From 4502eb13fffe043417d57f9e7bbdea5fd0c21c9c Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:15:48 +0200 Subject: [PATCH 3/8] Add getting-started, Arena, packs, and verification pages. These document the gym CLI and eval splits so users do not have to reverse-engineer the code. Co-authored-by: Cursor --- docs/api/python.md | 10 ++++++ docs/architecture/dynamic_topology.md | 17 ++++++++++ docs/architecture/multimodal_obs.md | 28 ++++++++++++++++ docs/architecture/ot_physics.md | 17 ++++++++++ docs/arena.md | 25 ++++++++++++++ docs/backends.md | 14 ++++++++ docs/baselines.md | 12 +++++++ docs/benchmarks/baselines.md | 23 +++++++++++++ docs/benchmarks/overview.md | 9 +++++ docs/benchmarks/run.md | 46 ++++++++++++++++++++++++++ docs/benchmarks/self_play.md | 26 +++++++++++++++ docs/capabilities.md | 9 +++++ docs/contributing.md | 16 +++++++++ docs/datasets.md | 13 ++++++++ docs/diagnostics/overview.md | 4 +++ docs/environment/difficulty.md | 7 ++++ docs/environment/graph_observations.md | 15 +++++++++ docs/environment/reproducibility.md | 5 +++ docs/getting-started.md | 37 +++++++++++++++++++++ docs/guides/verification.md | 18 ++++++++++ docs/interop/pettingzoo.md | 23 +++++++++++++ docs/interop/soc_export.md | 22 ++++++++++++ docs/product-direction.md | 19 +++++++++++ docs/python-quickstart.md | 45 +++++++++++++++++++++++++ docs/questions.md | 30 +++++++++++++++++ docs/research.md | 16 +++++++++ docs/roadmap.md | 9 +++++ docs/scenarios.md | 18 ++++++++++ docs/submissions.md | 22 ++++++++++++ docs/training/curriculum.md | 11 ++++++ 30 files changed, 566 insertions(+) create mode 100644 docs/api/python.md create mode 100644 docs/architecture/dynamic_topology.md create mode 100644 docs/architecture/multimodal_obs.md create mode 100644 docs/architecture/ot_physics.md create mode 100644 docs/arena.md create mode 100644 docs/backends.md create mode 100644 docs/baselines.md create mode 100644 docs/benchmarks/baselines.md create mode 100644 docs/benchmarks/overview.md create mode 100644 docs/benchmarks/run.md create mode 100644 docs/benchmarks/self_play.md create mode 100644 docs/capabilities.md create mode 100644 docs/contributing.md create mode 100644 docs/datasets.md create mode 100644 docs/diagnostics/overview.md create mode 100644 docs/environment/difficulty.md create mode 100644 docs/environment/graph_observations.md create mode 100644 docs/environment/reproducibility.md create mode 100644 docs/getting-started.md create mode 100644 docs/guides/verification.md create mode 100644 docs/interop/pettingzoo.md create mode 100644 docs/interop/soc_export.md create mode 100644 docs/product-direction.md create mode 100644 docs/python-quickstart.md create mode 100644 docs/questions.md create mode 100644 docs/research.md create mode 100644 docs/roadmap.md create mode 100644 docs/scenarios.md create mode 100644 docs/submissions.md create mode 100644 docs/training/curriculum.md diff --git a/docs/api/python.md b/docs/api/python.md new file mode 100644 index 0000000..c809214 --- /dev/null +++ b/docs/api/python.md @@ -0,0 +1,10 @@ +# Python API + +```{eval-rst} +.. automodule:: netforge_rl + :members: + :no-undoc-members: + :imported-members: +``` + +{doc}`environment` · {doc}`siem` · {doc}`nlp` · {doc}`sim2real` diff --git a/docs/architecture/dynamic_topology.md b/docs/architecture/dynamic_topology.md new file mode 100644 index 0000000..e1958aa --- /dev/null +++ b/docs/architecture/dynamic_topology.md @@ -0,0 +1,17 @@ +# Dynamic topologies + +`TopologyEventEngine` mutates the graph mid-episode: churn (online/offline), +migration (subnet/IP), arrival (BYOD from padding slots). + +Events: `host_offline`, `host_online`, `host_migrate`, `host_arrive`. + +```python +cfg = { + 'topology_churn_rate': 0.02, + 'topology_migration_rate': 0.01, + 'topology_arrival_rate': 0.005, +} +env = NetForgeRLEnv(cfg) +``` + +Rates also scale with {doc}`../training/curriculum`. diff --git a/docs/architecture/multimodal_obs.md b/docs/architecture/multimodal_obs.md new file mode 100644 index 0000000..563d5d3 --- /dev/null +++ b/docs/architecture/multimodal_obs.md @@ -0,0 +1,28 @@ +# Multi-modal obs + +```python +env = NetForgeRLEnv({'scenario_type': 'ransomware', 'pcap_obs': True}) +``` + +| Key | Shape | | +|---|---|---| +| `pcap` | (32, 20) | packet snapshot this tick | +| `node_features` | (100, 8) | per-host GNN matrix | +| `adj_matrix` | (10000,) | 100×100 routing (always) | + +PCAP dims 0–19: src/dst idx, protocol, port, payload, SYN/RST/ACK/PSH, lateral, +C2, recon, exfil, exploit, dst_sensitive, src_privilege, dst_compromised, +tick_norm, encrypted, severity. All in `[0, 1]`. + +Node dims: privilege, online, compromised, decoy, DC, subnet_type, cvss/10, EDR. + +Packets come from state (C2 beacons, lateral, then benign fill), not noise. + +```python +import torch +from torch_geometric.data import Data + +x = torch.tensor(obs['node_features']) +adj = torch.tensor(obs['adj_matrix'].reshape(100, 100)) +data = Data(x=x, edge_index=adj.nonzero().t().contiguous()) +``` diff --git a/docs/architecture/ot_physics.md b/docs/architecture/ot_physics.md new file mode 100644 index 0000000..2ee8b1c --- /dev/null +++ b/docs/architecture/ot_physics.md @@ -0,0 +1,17 @@ +# OT/SCADA physics + +First-order lag on `PLC_Firmware` hosts (`netforge_rl/scenarios/ot_physics.py`). +Ticked after actions, before termination. + +`x[t+1] = x[t] + (x_sp − x[t]) / τ + N(0, σ)` + +| | Nominal | τ | σ | Alarm | Critical | +|---|---|---|---|---|---| +| temperature °C | 40–60 | 20 | 0.1 | > 80 | > 120 | +| pressure bar | 90–110 | 10 | 0.2 | >130 or <70 | >180 or <30 | +| flow L/min | 40–60 | 5 | 0.3 | >90 or <20 | >150 or <5 | + +Alarm → SIEM severity 7. Critical → `kinetic_destruction`, severity 10, ends `ot_stuxnet`. + +`OverloadPLC` (id 20, Root on PLC) jumps setpoints. ~10–15 ticks to destruction. +`IsolateHost` stops physics on that PLC. diff --git a/docs/arena.md b/docs/arena.md new file mode 100644 index 0000000..dc7f257 --- /dev/null +++ b/docs/arena.md @@ -0,0 +1,25 @@ +# Evaluation splits + +Train, check, and hold out topologies with the same metrics: mission, SLA, +false positives, exfil, kinetic failures. + +```{image} _static/figures/arena.svg +:alt: train / dev / hidden splits +:class: fe-fig +``` + +```bash +netforge arena --json +netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 1 --max-ticks 40 +netforge benchmark --table --seeds 0 1 --max-ticks 40 +``` + +| Split | Public | Use | +|---|---|---| +| `train` | yes | learning | +| `dev` | yes | pick a checkpoint | +| `hidden` | organizer | official eval (repo ships a public proxy) | + +Blue is scored on keeping the network up without wrecking it (isolation / FPs). +Red eval can use a **population** (`random`, heuristic, kill-chain), not one +script. {doc}`submissions` · {doc}`questions` diff --git a/docs/backends.md b/docs/backends.md new file mode 100644 index 0000000..19b9d29 --- /dev/null +++ b/docs/backends.md @@ -0,0 +1,14 @@ +# Python vs JAX + +Python is the gym. JAX is a throughput surrogate — not a SIEM replica. + +| | Python | JAX | +|---|---|---| +| Role | Arena, SIEM, probes | massive rollouts | +| SIEM / NLP | yes | scalar alert | +| Durative / event time | yes | reduced core | +| Belief graphs | yes | host arrays (not yet) | +| Parity | golden trajectory | NumPy reference kernel | + +`tests/parity/test_jax_reference_equivalence.py`, `test_golden_trajectory.py`. +SIEM / deception / probes → Python. 10⁵ steps/s → JAX, state the subset. diff --git a/docs/baselines.md b/docs/baselines.md new file mode 100644 index 0000000..e84d0b7 --- /dev/null +++ b/docs/baselines.md @@ -0,0 +1,12 @@ +# Baselines + +In-tree: `random`, `heuristic-blue`, `heuristic-red`, `killchain-red`, +JAX IPPO (`netforge_rl.baselines.jax_ppo`). + +Your MAPPO / GNN / QMIX / LLM: train outside, evaluate here. + +```bash +netforge benchmark --table --split dev --seeds 0 1 --max-ticks 40 +``` + +Configs: `benchmarks/configs/`. Same split, seeds, and `max_ticks` when you compare runs. diff --git a/docs/benchmarks/baselines.md b/docs/benchmarks/baselines.md new file mode 100644 index 0000000..94e0f27 --- /dev/null +++ b/docs/benchmarks/baselines.md @@ -0,0 +1,23 @@ +# Baselines + +`netforge_rl.baselines`. + +| Policy | | +|---|---| +| `RandomPolicy` | uniform `MultiDiscrete([32, 100])` | +| `HeuristicBluePolicy` | isolate first compromised host, else analyse | +| `HeuristicRedPolicy` | exploit without recon — usually 0 compromises | +| `KillChainRedPolicy` | recon → exploit → pivot; ~2–3 hosts/ep, SLA drops | +| `jax_ppo` | on-device IPPO (`baselines/jax_ppo.py`) | + +`ExploitRemoteService` needs prior `DiscoverNetworkServices`. Use kill-chain as +the Red reference, not heuristic-red. + +```bash +python -m benchmarks.build_leaderboard --episodes 5 --max-steps 150 +python -m benchmarks.train_curve --name blue_ransomware --iters 40 +``` + +Committed IPPO on ransomware: mean reward **0.06 → 0.71** (40 iters, CPU). +Load with `jax_ppo.load_params`. RLlib: `benchmarks/rllib_rmappo.py`. +{doc}`run` · {doc}`self_play` diff --git a/docs/benchmarks/overview.md b/docs/benchmarks/overview.md new file mode 100644 index 0000000..e88104b --- /dev/null +++ b/docs/benchmarks/overview.md @@ -0,0 +1,9 @@ +# Benchmarks + +`benchmarks/` scripts, `benchmarks/results/` artifacts. + +- [Baselines](baselines.md) +- [Self-play / Elo](self_play.md) +- [Run](run.md) — throughput, eval, sweeps, env spec + +JAX CPU, batch 4096: **270,795** env-steps/s (1,083,181 agent-steps/s). Reproduce in [Run](run.md). diff --git a/docs/benchmarks/run.md b/docs/benchmarks/run.md new file mode 100644 index 0000000..1b11f92 --- /dev/null +++ b/docs/benchmarks/run.md @@ -0,0 +1,46 @@ +# Run + +```bash +python -m benchmarks.throughput --batches 1024 4096 --steps 50 --warmup 2 +``` + +CPU. `agent-steps/s = env-steps/s × agents`. + +| Backend | Batch | env-steps/s | agent-steps/s | +|---|---|---|---| +| Python | 1 | ~300 | ~1,200 | +| JAX | 1024 | 158,540 | 634,160 | +| JAX | 4096 | 270,795 | 1,083,181 | + +## Competition + +`benchmarks/competition_eval.py` → `results/leaderboard.json`. + +Blue: `SLA×50 − compromised×2 − MTTC×0.1 + blue_reward×0.1` +Red: `compromised×2 + exfil×0.01 − SLA×10 + red_reward×0.1` + +```bash +python -m benchmarks.competition_eval --name my_agent --team blue --episodes 10 +python -m benchmarks.run_benchmark --name heuristic --team blue --seeds 20 +python -m benchmarks.run_benchmark --name killchain --team red --red killchain --blue heuristic --seeds 20 +python -m benchmarks.run_benchmark --name killchain --team red --red killchain --gap --seeds 20 +python -m benchmarks.build_leaderboard --episodes 5 --max-steps 150 +python -m benchmarks.env_spec --json +``` + +| `info` key | | +|---|---| +| `compromised_hosts` / `isolated_hosts` / `active_hosts` | padding excluded | +| `SLA_Uptime_Percentage` | healthy online fraction | +| `MTTC` / `containment_time` | time to contain | +| `detection_rate` | compromises that were isolated | +| `Total_Exfiltrated_Data` | cumulative | +| `false_positives` / `successful_exploits` / `services_restored` | step outcomes | +| `deception_hits` / `deception_efficacy` | decoy / honeytoken | +| `attack_techniques` / `attack_coverage` | ATT&CK ids | +| `blue_score` / `red_score` / `normalized_reward` | composites | +| `agent_energy` | remaining budget | +| `information_asymmetry` | under `DiagnosticsWrapper` | +| `__curriculum__` | phase / mean_reward / … | + +{doc}`baselines` diff --git a/docs/benchmarks/self_play.md b/docs/benchmarks/self_play.md new file mode 100644 index 0000000..6f85081 --- /dev/null +++ b/docs/benchmarks/self_play.md @@ -0,0 +1,26 @@ +# Self-play / Elo + +One ladder: Blue score = mean SLA, Red = `1 − SLA`. `benchmarks/self_play.py`. + +```bash +python -m benchmarks.self_play --seeds 5 --max-ticks 150 +``` + +```python +from benchmarks.self_play import population_tournament + +result = population_tournament( + scenarios=['ransomware', 'apt_espionage', 'cloud_hybrid'], + seeds=list(range(5)), + max_ticks=150, +) +``` + +Default pools: `random`, `heuristic`, `killchain-red`. Pass `red_pool` / `blue_pool` +(`name → factory`) to add trained agents. + +``` + 1. heuristic-blue 1041.7 + 2. random-blue 1022.1 + 3. killchain-red 980.4 +``` diff --git a/docs/capabilities.md b/docs/capabilities.md new file mode 100644 index 0000000..609dd71 --- /dev/null +++ b/docs/capabilities.md @@ -0,0 +1,9 @@ +# Capability tests + +```bash +netforge test-policy --policy heuristic-blue --out cards/ +``` + +Scores in `[0, 1]`: Safety, OOD, Memory, FPs, Temporal, Deception, Adaptation, +Attention. Radar if matplotlib is installed. Full-state graphs stay on +`DiagnosticsWrapper`. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..45fc651 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,16 @@ +# Contribute + +[CONTRIBUTING.md](https://github.com/reforcemind/NetForge_RL/blob/main/CONTRIBUTING.md) · +[Code of Conduct](https://github.com/reforcemind/NetForge_RL/blob/main/CODE_OF_CONDUCT.md) + +Packs, probes, wrappers. Trainers stay out of tree. Blue sees SIEM. + +Gate: `scripts/verify_all.sh` / `scripts/verify_local.ps1`. +CI builds the Sphinx site; `docs.yml` deploys it to GitHub Pages on `main`. + +| | | +|---|---| +| Probe | {doc}`questions` | +| YAML pack | {doc}`scenarios` | +| Verify | {doc}`guides/verification` | +| Policy | {doc}`submissions` | diff --git a/docs/datasets.md b/docs/datasets.md new file mode 100644 index 0000000..a89b333 --- /dev/null +++ b/docs/datasets.md @@ -0,0 +1,13 @@ +# Offline RL datasets + +Git extra `offline`. Hugging Face dumps are not published yet. + +```bash +pip install 'netforge-rl[offline]' +netforge collect --policy heuristic-blue --red killchain-red \ + --episodes 8 --quality mixed --out netforge_offline.npz +netforge collect --hdf5 --out netforge_offline.hdf5 +``` + +`export_npz` / `export_hdf5` are Minari-shaped without importing `minari`. +Mix random / heuristic / RL / self-play / LLM; tag quality before fine-tune. diff --git a/docs/diagnostics/overview.md b/docs/diagnostics/overview.md new file mode 100644 index 0000000..18340c3 --- /dev/null +++ b/docs/diagnostics/overview.md @@ -0,0 +1,4 @@ +# Diagnostics + +{doc}`/capabilities`. `DiagnosticsWrapper` adds `oracle_obs`, +`information_asymmetry`, `oracle_graph`. diff --git a/docs/environment/difficulty.md b/docs/environment/difficulty.md new file mode 100644 index 0000000..bf97cc4 --- /dev/null +++ b/docs/environment/difficulty.md @@ -0,0 +1,7 @@ +# Difficulty and splits + +Presets: `easy` / `medium` / `hard` + frozen `EVAL_SEEDS`. + +- **train** — public seeds, `evaluation_mode=False` +- **dev** — `EVAL_SEEDS`, `evaluation_mode=True` +- **hidden** — organizer seed block diff --git a/docs/environment/graph_observations.md b/docs/environment/graph_observations.md new file mode 100644 index 0000000..638bc19 --- /dev/null +++ b/docs/environment/graph_observations.md @@ -0,0 +1,15 @@ +# Belief-state graphs + +Blue’s graph is reconstructed from inventory + SIEM. Red’s is fog of war. +The privileged map is for diagnostics, not for training Blue. + +| Mode | Who | | +|---|---|---| +| `belief` | Blue (default) | assets + alerts | +| `fog` | Red | discovered hosts | +| `oracle` | diagnostics | true compromise | + +`GraphObservationWrapper` sets `info["graph"]`. +`oracle=True` also sets `info["oracle_graph"]`. +GNN policies read `node_features`, `edge_index`, `edge_attr`, `node_mask`. +The PettingZoo `obs` vector is unchanged. diff --git a/docs/environment/reproducibility.md b/docs/environment/reproducibility.md new file mode 100644 index 0000000..22cfee6 --- /dev/null +++ b/docs/environment/reproducibility.md @@ -0,0 +1,5 @@ +# Reproducibility + +Fixed seed replays obs, SIEM embeddings, infos, rewards. +`tests/parity/test_seeded_observation_replay.py` + golden trajectory. +Pin package + Arena version + config. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..d895fa8 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,37 @@ +# Getting Started + +Python 3.12+. Install from git (not PyPI). + +```bash +python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL' +netforge run hospital_ransomware --replay replay.html +``` + +That spins Red vs Blue on a hospital ransomware pack and writes an HTML replay +of hosts, SIEM, and rewards. + +```bash +git clone https://github.com/reforcemind/NetForge_RL.git +cd NetForge_RL +pip install -e ".[dev]" +``` + +```python +import netforge_rl +from pettingzoo import make +from netforge_rl.environment.graph_wrapper import GraphObservationWrapper + +env = GraphObservationWrapper(make("parallel", "netforge/ransomware-v4", max_ticks=80)) +obs, infos = env.reset(seed=0) +``` + +`infos["blue_dmz"]["graph"]` is what Blue believes from SIEM. +One Blue vs a scripted attacker: `gym.make("NetForge/Blue-v4")`. + +| | | +|---|---| +| Env ids | [Python](python-quickstart) | +| Red / Blue / SIEM | [Cyber](cybersec/overview) | +| Packs | [Scenarios](scenarios) | +| Your policy | [Submissions](submissions) | +| Gate | [Verification](guides/verification) | diff --git a/docs/guides/verification.md b/docs/guides/verification.md new file mode 100644 index 0000000..3e39392 --- /dev/null +++ b/docs/guides/verification.md @@ -0,0 +1,18 @@ +# Verification + +```bash +./scripts/verify_all.sh # Linux / Git Bash +./scripts/verify_local.ps1 # Windows +``` + +```bash +pip install -e ".[dev,docs]" +ruff check . && ruff format --check . +pytest tests/ -m fast +sphinx-build -b html -W --keep-going docs docs/_build/html +``` + +CI: ruff + pytest + Sphinx HTML (`ci.yml`). Pages: `docs.yml` deploys to GitHub Pages on `main`. +Do not vendor MAPPO/QMIX/CT-GMARL. + +{doc}`/getting-started` · {doc}`/python-quickstart` · {doc}`/contributing` diff --git a/docs/interop/pettingzoo.md b/docs/interop/pettingzoo.md new file mode 100644 index 0000000..4110985 --- /dev/null +++ b/docs/interop/pettingzoo.md @@ -0,0 +1,23 @@ +# PettingZoo and Gymnasium + +```python +import netforge_rl +import gymnasium as gym +from pettingzoo import make + +env = make("parallel", "netforge/ransomware-v4", max_ticks=200) +single = gym.make("NetForge/Blue-v4", max_ticks=200) +``` + +| API | Id | +|---|---| +| PettingZoo parallel | `netforge/ransomware-v4`, `netforge/arena-v4` | +| PettingZoo AEC | same ids | +| Gymnasium | `NetForge/Blue-v4`, alias `NetForge-v4` | + +`gym.make("netforge_rl:NetForge/Blue-v4")` also works. Mask is flat `int8[132]`; +`CyberActionSpace.sample(mask=…)` accepts it. + +Farama listing (docs PR to PettingZoo `third_party_envs`): Parallel + AEC +`netforge/ransomware-v4`, Gymnasium `NetForge/Blue-v4`, +https://reforcemind.github.io/NetForge_RL/ diff --git a/docs/interop/soc_export.md b/docs/interop/soc_export.md new file mode 100644 index 0000000..a9d6492 --- /dev/null +++ b/docs/interop/soc_export.md @@ -0,0 +1,22 @@ +# SOC export (OCSF) + +`record_siem=True` keeps the full stream. `export_ocsf` writes JSONL. + +```python +from netforge_rl.environment.parallel_env import NetForgeRLEnv +from netforge_rl.siem.export import export_ocsf, siem_to_ocsf + +env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 100, 'record_siem': True}) +env.reset(seed=0) +export_ocsf(env, 'runs/episode.ocsf.jsonl') +siem_to_ocsf(log_line, subnet='10.0.0.0/24', tick=42) +``` + +| EventID | OCSF | +|---|---| +| 4624 / 4625 / 4648 / 4768 / 4776 | Authentication (3002) | +| 4688 / Sysmon 1 / 10 | Process (1007) | +| Sysmon 3 | Network (4001) | +| Sysmon 22 | DNS (4003) | + +Raw XML stays in `raw_event`. Honeytoken / incident → higher `severity_id`. diff --git a/docs/product-direction.md b/docs/product-direction.md new file mode 100644 index 0000000..3c1ab5a --- /dev/null +++ b/docs/product-direction.md @@ -0,0 +1,19 @@ +# Scope + +A gym for training cyber-defense agents. Grow packs, metrics, and wrappers — +not host-type count and not a bundled MAPPO. + +```{image} _static/figures/status.svg +:alt: What ships in the gym +:class: fe-fig +``` + +| In this repo | Bring your own | +|---|---| +| Network, SIEM, Red/Blue actions | MAPPO / QMIX / GNN trainers | +| Belief graphs from logs | Privileged Blue graphs at train time | +| YAML packs | Weekly ATT&CK dumps | +| Heuristic / kill-chain baselines | Production SOC connectors | + +Sister docs kit: [FlowEdge](https://github.com/reforcemind/FlowEdge). +{doc}`questions` · {doc}`arena` diff --git a/docs/python-quickstart.md b/docs/python-quickstart.md new file mode 100644 index 0000000..0105215 --- /dev/null +++ b/docs/python-quickstart.md @@ -0,0 +1,45 @@ +# Python + +`import netforge_rl` registers the ids. CPython 3.12+. Not on PyPI. + +```bash +python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL' +# from a clone: +python -m pip install -e ".[dev]" +``` + +```python +import netforge_rl +import gymnasium as gym +from pettingzoo import make + +env = make("parallel", "netforge/ransomware-v4", max_ticks=200) +obs, infos = env.reset(seed=0) +actions = {agent: env.action_space(agent).sample() for agent in env.agents} +obs, rewards, term, trunc, infos = env.step(actions) + +blue = gym.make("NetForge/Blue-v4", max_ticks=200) +``` + +| API | Id | | +|---|---|---| +| PettingZoo parallel | `netforge/ransomware-v4` | 1 Red + 3 Blue | +| PettingZoo parallel | `netforge/arena-v4` | same factory | +| PettingZoo AEC | same ids | turn-based | +| Gymnasium | `NetForge/Blue-v4` | one Blue vs scripted Red | +| Gymnasium | `NetForge-v4` | alias | + +Action: `MultiDiscrete([32, 100])`, mask `int8[132]`. +Blue graph: `GraphObservationWrapper`. + +| Extra | | +|---|---| +| *(none)* | env, CLI, heuristics | +| `dev` | ruff, pytest | +| `docs` | Sphinx | +| `jax` | vectorized surrogate | +| `rllib` | RLlib env wrapper | +| `offline` | HDF5 export | +| `nlp` / `llm_clients` | SIEM encoder / LLM agents | + +{doc}`interop/pettingzoo` diff --git a/docs/questions.md b/docs/questions.md new file mode 100644 index 0000000..5bad5c1 --- /dev/null +++ b/docs/questions.md @@ -0,0 +1,30 @@ +# Probes + +Small experiments the gym already runs. Heuristics are in-tree so they execute. + +```{image} _static/figures/belief.svg +:alt: SIEM belief vs full state +:class: fe-fig +``` + +```bash +netforge questions +netforge questions belief-vs-oracle --seeds 0 --max-ticks 40 +``` + +| id | What it checks | +|---|---| +| `belief-vs-oracle` | How much compromise is missing from Blue’s SIEM graph | +| `reward-design` | SLA-only / FP-penalized reward vs isolation | +| `red-population` | One Blue vs several Reds | +| `risk-sensitive` | Mean vs CVaR vs kinetic tails | +| `time-mode` | Event-driven vs fixed-step | +| `ood-topology` | More live hosts than training | +| `adaptation` | Red changes strategy mid-episode | +| `deception` | Honeypots / decoys | +| `constrained-sla` | Defense under SLA / FP costs | +| `gnn-belief` | Train a GNN on SIEM belief graphs (your trainer) | +| `offline-to-online` | Pretrain on dumps, then online | +| `league-psro` | Best response to a Red league | + +Add one in `netforge_rl/arena/questions/`. {doc}`submissions` diff --git a/docs/research.md b/docs/research.md new file mode 100644 index 0000000..f275b8f --- /dev/null +++ b/docs/research.md @@ -0,0 +1,16 @@ +# Directions + +This tree is the gym: network, logs, actions, eval. Trainers and new exploit +families stay out unless they are a wrapper, pack, or probe. + +- Belief vs oracle (SOC view vs true compromise) +- Defense under SLA / false-positive cost +- Reward that does not isolate the whole net +- Offline logs → online fine-tune +- Adaptive Red, leagues +- Event time vs fixed ticks +- LLM vs RL on the same env + +Catalog: `netforge questions`. Issues: [RESEARCH.md](https://github.com/reforcemind/NetForge_RL/blob/main/RESEARCH.md). + +{doc}`product-direction` · {doc}`roadmap` diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..fc91e28 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,9 @@ +# Roadmap + +Backlog: [RESEARCH.md](https://github.com/reforcemind/NetForge_RL/blob/main/RESEARCH.md). +Labels: `research` / `good first issue`. + +Shipped: PettingZoo / Gymnasium ids, SIEM belief graphs, eval CLI. + +Next: trainers **on** this env (out of tree), hidden-split harness, Farama listing, +published checkpoints. diff --git a/docs/scenarios.md b/docs/scenarios.md new file mode 100644 index 0000000..47a6d67 --- /dev/null +++ b/docs/scenarios.md @@ -0,0 +1,18 @@ +# Scenarios + +Pick a family and a topology. No simulator patch. + +Families: `ransomware`, `apt_espionage`, `cloud_hybrid`, `iot_grid`, `ot_stuxnet`. + +Packs: `hospital_ransomware`, `cloud_iam`, `enterprise_apt`, `finance`, +`iot_fleet`, `ot_plant`, `zero_trust`. + +```bash +netforge run hospital_ransomware +netforge run examples/my_scenario.yaml +``` + +Reward knobs (`RewardDesignWrapper`): `sla_only`, `security_only`, +`fp_penalized`, `constrained_sla`, `safety_critical`, `sparse`. + +YAML lives in `netforge_rl/scenarios/packs/`. diff --git a/docs/submissions.md b/docs/submissions.md new file mode 100644 index 0000000..731615e --- /dev/null +++ b/docs/submissions.md @@ -0,0 +1,22 @@ +# Your agent + +Drop in a Blue policy with `make_blue`. Eval reports mission, SLA, FPs, security, +kinetic failures — not only return. + +```bash +netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 1 --max-ticks 40 +``` + +```python +NAME, TEAM, TECHNIQUE = "my-blue", "lab", "ppo-belief" + +def wrap(env): + return env # belief graphs already on; do not attach oracle_graph + +def make_blue(seed=0): + return MyPolicy(seed=seed) +``` + +`act(env, agent_id) -> np.ndarray`. Optional `make_red`. `--card` adds probes. + +{doc}`arena` · `examples/submissions/heuristic_blue.py` diff --git a/docs/training/curriculum.md b/docs/training/curriculum.md new file mode 100644 index 0000000..7932681 --- /dev/null +++ b/docs/training/curriculum.md @@ -0,0 +1,11 @@ +# Curriculum + +`CurriculumWrapper` raises difficulty when a moving-average reward clears a threshold. + +| Phase | Hosts | Scenarios | Mechanics | +|---|---|---|---| +| Novice | 5 | ransomware | static, 3× reward | +| Intermediate | 25 | + apt | DHCP every 80 ticks, 1.5× | +| Expert | 100 | + iot_grid | DHCP 40, dynamic topology, 1× | + +{doc}`../architecture/dynamic_topology` From cc825be25ed51d9c2f6b2d540047c10397425618 Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:15:48 +0200 Subject: [PATCH 4/8] Add the FlowEdge parchment/wine theme and Excalidraw figures. Drop the Light/Dark/Auto picker so Furo follows the browser color scheme only. Co-authored-by: Cursor --- docs/_static/custom.css | 523 +++++++++ docs/_static/custom.js | 76 ++ docs/_static/figures/arena.excalidraw | 390 +++++++ docs/_static/figures/arena.svg | 12 + docs/_static/figures/belief.excalidraw | 225 ++++ docs/_static/figures/belief.svg | 12 + docs/_static/figures/build.py | 547 +++++++++ docs/_static/figures/mark.svg | 10 + docs/_static/figures/purpose.excalidraw | 390 +++++++ docs/_static/figures/purpose.svg | 14 + docs/_static/figures/status.excalidraw | 1105 +++++++++++++++++++ docs/_static/figures/status.svg | 22 + docs/_static/figures/why-this.excalidraw | 1281 ++++++++++++++++++++++ docs/_static/figures/why-this.svg | 26 + docs/_static/figures/workflow.excalidraw | 797 ++++++++++++++ docs/_static/figures/workflow.svg | 18 + docs/_templates/page.html | 17 + 17 files changed, 5465 insertions(+) create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/custom.js create mode 100644 docs/_static/figures/arena.excalidraw create mode 100644 docs/_static/figures/arena.svg create mode 100644 docs/_static/figures/belief.excalidraw create mode 100644 docs/_static/figures/belief.svg create mode 100644 docs/_static/figures/build.py create mode 100644 docs/_static/figures/mark.svg create mode 100644 docs/_static/figures/purpose.excalidraw create mode 100644 docs/_static/figures/purpose.svg create mode 100644 docs/_static/figures/status.excalidraw create mode 100644 docs/_static/figures/status.svg create mode 100644 docs/_static/figures/why-this.excalidraw create mode 100644 docs/_static/figures/why-this.svg create mode 100644 docs/_static/figures/workflow.excalidraw create mode 100644 docs/_static/figures/workflow.svg create mode 100644 docs/_templates/page.html diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..1aa87e0 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,523 @@ +@import url('https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600;6..72,700&family=Libre+Franklin:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +:root { + --fe-wine: #7b2733; + --fe-wine-deep: #5e1f2b; + --fe-ink: #2b2521; + --fe-shadow: 0 16px 40px rgba(43, 31, 20, 0.16); + --fe-shadow-soft: 0 8px 22px rgba(43, 31, 20, 0.1); + --fe-radius: 16px; +} + +html[data-theme="dark"], +body[data-theme="dark"], +html.fe-theme-dark { + --fe-wine: #dd94a0; + --fe-wine-deep: #e3a6b0; + --fe-ink: #ece0cb; + --fe-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); + --fe-shadow-soft: 0 8px 22px rgba(0, 0, 0, 0.28); +} + +@media (prefers-color-scheme: dark) { + html:not([data-theme="light"]):not(.fe-theme-dark) { + --fe-wine: #dd94a0; + --fe-wine-deep: #e3a6b0; + --fe-ink: #ece0cb; + --fe-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); + --fe-shadow-soft: 0 8px 22px rgba(0, 0, 0, 0.28); + } +} + +body { + --font-stack: 'Libre Franklin', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --font-stack--monospace: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + --font-stack--headings: 'Newsreader', Georgia, 'Times New Roman', serif; +} + +.content { + background: + radial-gradient(900px 380px at 12% -10%, rgba(123, 39, 51, 0.09), transparent 62%), + radial-gradient(720px 320px at 92% 0%, rgba(196, 146, 64, 0.08), transparent 58%); +} + +.fe-home .content { + background: + radial-gradient(1100px 520px at 18% -18%, rgba(123, 39, 51, 0.14), transparent 58%), + radial-gradient(900px 420px at 88% -8%, rgba(196, 146, 64, 0.12), transparent 52%), + var(--color-background-primary); +} + +h1, h2, h3, h4, h5, h6, .sidebar-brand-text { + font-family: var(--font-stack--headings); + font-weight: 600; + letter-spacing: -0.01em; +} +h1 { font-size: 2.8rem; line-height: 1.06; font-weight: 700; margin-top: 0.35rem; margin-bottom: 0.85rem; } +h2 { + font-size: 1.75rem; + margin-top: 2.6rem; + padding-bottom: 0.35rem; + border-bottom: 1px solid var(--color-background-border); +} +h3 { font-size: 1.3rem; } + +article { line-height: 1.72; } + +.content-icon-container { + z-index: 20; +} + +.theme-toggle-container { + display: none !important; +} + +/* —— Site nav —— */ +.fe-topnav { + position: relative; + top: auto; + z-index: 8; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: 0.55rem 0.8rem; + margin: 0 -0.4rem 1.35rem; + padding: 0.55rem 0.4rem 0.7rem; + background: var(--color-background-primary); + border-bottom: 1px solid var(--color-background-border); + box-shadow: 0 10px 24px color-mix(in srgb, var(--color-background-primary) 80%, transparent); +} +.fe-topnav-inner { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + padding: 0.1rem 0; +} +.fe-topnav-link { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + padding: 0.34rem 0.7rem; + border-radius: 999px; + border: 1px solid var(--color-background-border); + background: var(--color-background-secondary); + color: var(--color-foreground-secondary); + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.01em; + text-decoration: none !important; + transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease; +} +.fe-topnav-link:hover { + color: var(--color-brand-primary); + border-color: color-mix(in srgb, var(--color-brand-primary) 45%, var(--color-background-border)); + transform: translateY(-1px); +} +.fe-topnav-link.is-current { + background: var(--fe-wine); + border-color: var(--fe-wine); + color: #f7efdc; +} +html[data-theme="dark"] .fe-topnav-link.is-current, +body[data-theme="dark"] .fe-topnav-link.is-current, +html.fe-theme-dark .fe-topnav-link.is-current { + color: #211b19; +} +html[data-theme="dark"] .fe-chips li, +body[data-theme="dark"] .fe-chips li, +html.fe-theme-dark .fe-chips li { + color: #f0b4bc; + background: #3a2c2a; + border-color: #5a4440; +} +html[data-theme="dark"] .fe-metric, +html[data-theme="dark"] .fe-jump-card, +html[data-theme="dark"] .fe-card, +html[data-theme="dark"] .fe-explorer, +body[data-theme="dark"] .fe-metric, +body[data-theme="dark"] .fe-jump-card, +body[data-theme="dark"] .fe-card, +body[data-theme="dark"] .fe-explorer, +html.fe-theme-dark .fe-metric, +html.fe-theme-dark .fe-jump-card, +html.fe-theme-dark .fe-card, +html.fe-theme-dark .fe-explorer { + background: #322824; + border-color: #4a3c36; +} +html[data-theme="dark"] .fe-metric-value, +html[data-theme="dark"] .fe-jump-kicker, +body[data-theme="dark"] .fe-metric-value, +body[data-theme="dark"] .fe-jump-kicker, +html.fe-theme-dark .fe-metric-value, +html.fe-theme-dark .fe-jump-kicker { + color: #f0b4bc; +} +html[data-theme="dark"] .fe-jump-card strong, +body[data-theme="dark"] .fe-jump-card strong, +html.fe-theme-dark .fe-jump-card strong { + color: #f3e6d2; +} +html[data-theme="dark"] .fe-jump-card span:last-child, +html[data-theme="dark"] .fe-metric-label, +body[data-theme="dark"] .fe-jump-card span:last-child, +body[data-theme="dark"] .fe-metric-label, +html.fe-theme-dark .fe-jump-card span:last-child, +html.fe-theme-dark .fe-metric-label { + color: #cbbba4; +} + +@media (prefers-color-scheme: dark) { + html:not([data-theme="light"]) .fe-metric, + html:not([data-theme="light"]) .fe-jump-card, + html:not([data-theme="light"]) .fe-card, + html:not([data-theme="light"]) .fe-explorer, + body:not([data-theme="light"]) .fe-metric, + body:not([data-theme="light"]) .fe-jump-card, + body:not([data-theme="light"]) .fe-card, + body:not([data-theme="light"]) .fe-explorer { + background: #322824; + border-color: #4a3c36; + } +} + +.content h1 { + margin-top: 0.35rem; +} + +/* —— Hero —— */ +.fe-hero-img, +.fe-fig, +img.fe-fig, +.figure img { + width: 100%; + max-width: 960px; + display: block; + border-radius: var(--fe-radius); + border: 1px solid var(--color-background-border); + box-shadow: var(--fe-shadow); + margin: 0.4rem auto 1.6rem; +} +.fe-hero-img { + box-shadow: 0 18px 48px rgba(43, 31, 20, 0.28); +} + +.fe-lede { + font-size: 1.22rem; + line-height: 1.6; + color: var(--color-foreground-secondary); + max-width: 46rem; + clear: both; +} + +.fe-chips { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + list-style: none; + margin: 0.2rem 0 1.35rem; + padding: 0; +} +.fe-chips li { + font-size: 0.78rem; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--color-brand-primary); + background: color-mix(in srgb, var(--color-brand-primary) 10%, var(--color-background-secondary)); + border: 1px solid color-mix(in srgb, var(--color-brand-primary) 22%, var(--color-background-border)); + border-radius: 999px; + padding: 0.32rem 0.72rem; + animation: fe-chip-in 0.5s ease both; +} +.fe-chips li:nth-child(2) { animation-delay: 0.06s; } +.fe-chips li:nth-child(3) { animation-delay: 0.12s; } +.fe-chips li:nth-child(4) { animation-delay: 0.18s; } + +.fe-metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.8rem; + margin: 0.4rem 0 1.8rem; +} +.fe-metric { + background: var(--color-background-secondary); + border: 1px solid var(--color-background-border); + border-radius: 14px; + padding: 0.95rem 1.05rem 1.05rem; + box-shadow: var(--fe-shadow-soft); + position: relative; + overflow: hidden; + animation: fe-chip-in 0.55s ease both; +} +.fe-metric:nth-child(2) { animation-delay: 0.07s; } +.fe-metric:nth-child(3) { animation-delay: 0.14s; } +.fe-metric::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 4px; + background: var(--fe-wine); +} +.fe-metric-value { + display: block; + font-family: var(--font-stack--headings); + font-size: 1.45rem; + font-weight: 700; + color: var(--color-brand-primary); + letter-spacing: -0.02em; + line-height: 1.15; +} +.fe-metric-label { + display: block; + margin-top: 0.28rem; + font-size: 0.82rem; + color: var(--color-foreground-muted); +} + +/* —— Jump cards —— */ +.fe-jump { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 0.85rem; + margin: 0.2rem 0 2rem; +} +.fe-jump-card { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-height: 8.4rem; + padding: 1rem 1.05rem 1.1rem; + border-radius: 14px; + border: 1px solid var(--color-background-border); + background: var(--color-background-secondary); + box-shadow: var(--fe-shadow-soft); + text-decoration: none !important; + color: inherit; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + animation: fe-chip-in 0.55s ease both; +} +.fe-jump-card:nth-child(1) { animation-delay: 0.04s; } +.fe-jump-card:nth-child(2) { animation-delay: 0.08s; } +.fe-jump-card:nth-child(3) { animation-delay: 0.12s; } +.fe-jump-card:nth-child(4) { animation-delay: 0.16s; } +.fe-jump-card:nth-child(5) { animation-delay: 0.2s; } +.fe-jump-card:nth-child(6) { animation-delay: 0.24s; } +.fe-jump-card:hover { + transform: translateY(-4px); + border-color: color-mix(in srgb, var(--color-brand-primary) 40%, var(--color-background-border)); + box-shadow: 0 16px 32px rgba(123, 39, 51, 0.14); +} +.fe-jump-kicker { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--color-brand-primary); +} +.fe-jump-card strong { + font-family: var(--font-stack--headings); + font-size: 1.18rem; + font-weight: 650; + color: var(--color-foreground-primary); +} +.fe-jump-card span:last-child { + font-size: 0.9rem; + color: var(--color-foreground-muted); + line-height: 1.45; +} + +/* —— News timeline —— */ +.fe-news { + list-style: none; + margin: 0.6rem 0 1.4rem; + padding: 0; +} +.fe-news-item { + position: relative; + padding: 0.85rem 0 0.95rem 1.25rem; + border-left: 2px solid color-mix(in srgb, var(--color-brand-primary) 35%, var(--color-background-border)); +} +.fe-news-item::before { + content: ""; + position: absolute; + left: -5px; + top: 1.15rem; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--fe-wine); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-brand-primary) 16%, transparent); +} +.fe-news-date { + display: inline-block; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-brand-primary); + margin-bottom: 0.2rem; +} +.fe-news-item p { margin: 0.15rem 0 0; } + +.fe-inline-links { + display: flex; + flex-wrap: wrap; + gap: 0.55rem 1rem; + margin: 0.3rem 0 1.6rem; + font-size: 0.95rem; +} + +/* —— Existing card grids —— */ +.fe-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(155px, 1fr)); + gap: 0.9rem; + margin: 1.5rem 0; +} +.fe-card { + background: var(--color-background-secondary); + border: 1px solid var(--color-background-border); + border-radius: 12px; + padding: 0.9rem 1.1rem; + box-shadow: var(--fe-shadow-soft); +} +.fe-card h4 { + font-family: var(--font-stack--headings); + color: var(--color-brand-primary); + margin: 0 0 0.5rem; + font-size: 1.06rem; +} +.fe-card ul { list-style: none; margin: 0; padding: 0; } +.fe-card li { + font-size: 0.92rem; + padding: 0.16rem 0 0.16rem 1.35rem; + position: relative; + color: var(--color-foreground-muted); +} +.fe-card li.done { color: var(--color-foreground-primary); } +.fe-card li::before { content: "\25CB"; position: absolute; left: 0; color: var(--color-foreground-muted); } +.fe-card li.done::before { content: "\2713"; color: var(--color-brand-primary); font-weight: 700; } + +.fe-explorer-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + gap: 0.9rem; + margin: 1.4rem 0; +} +.fe-explorer { + background: var(--color-background-secondary); + border: 1px solid var(--color-background-border); + border-radius: 12px; + padding: 0; + overflow: hidden; + box-shadow: var(--fe-shadow-soft); +} +.fe-explorer summary { + cursor: pointer; + color: var(--color-brand-primary); + font-family: var(--font-stack--headings); + font-size: 1.08rem; + font-weight: 600; + padding: 0.85rem 1rem; +} +.fe-explorer summary:hover { background: var(--color-background-hover); } +.fe-explorer[open] summary { border-bottom: 1px solid var(--color-background-border); } +.fe-explorer p { margin: 0; padding: 0.55rem 1rem; font-size: 0.92rem; } +.fe-explorer p + p { padding-top: 0; } +.fe-explorer code { font-size: 0.88em; } + +/* —— Sidebar —— */ +.sidebar-logo-container { margin: 0.7rem 0 0.4rem; } +.sidebar-logo { + max-width: 100%; + border-radius: 10px; + border: 1px solid var(--color-background-border); + box-shadow: 0 8px 26px rgba(43, 31, 20, 0.22); + transition: transform 0.35s ease; +} +.sidebar-brand:hover .sidebar-logo { transform: translateY(-2px) rotate(-1.5deg); } +.sidebar-brand-text { font-size: 1.4rem; letter-spacing: 0; } + +.sidebar-tree { line-height: 1.5; } +.sidebar-tree .reference { font-size: 0.92rem; } +.sidebar-tree .caption, +.sidebar-tree .caption-text, +.sidebar-tree p.caption { + font-family: var(--font-stack) !important; + font-size: 0.68rem !important; + font-weight: 700 !important; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--color-brand-primary) !important; + margin-top: 1.15rem; +} +.sidebar-tree li.current-page > .reference, +.sidebar-tree .current-page > a { + font-weight: 650; +} + +.highlight { border-radius: 10px; border: 1px solid var(--color-background-border); } +code.literal { border-radius: 5px; padding: 0.1em 0.4em; border: 1px solid var(--color-background-border); } + +a.reference { text-decoration: none; border-bottom: 1px solid transparent; } +a.reference:hover { border-bottom-color: currentColor; } + +blockquote { + border-left: 3px solid var(--color-brand-primary); + font-style: italic; + color: var(--color-foreground-secondary); +} + +article table { + border-collapse: collapse; + overflow: hidden; + border-radius: 12px; + box-shadow: var(--fe-shadow-soft); +} +article table th { + background: color-mix(in srgb, var(--color-brand-primary) 10%, var(--color-background-secondary)); +} + +.related-pages { + margin-top: 2.4rem; +} + +/* —— Motion —— */ +.fe-reveal { + opacity: 0; + transform: translateY(14px); + transition: opacity 0.55s ease, transform 0.55s ease; +} +.fe-reveal.is-in { + opacity: 1; + transform: none; +} + +@keyframes fe-hero-in { + from { opacity: 0; transform: translateY(10px) scale(0.985); } + to { opacity: 1; transform: none; } +} +@keyframes fe-chip-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: none; } +} +@keyframes fe-nav-in { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: none; } +} + +@media (prefers-reduced-motion: reduce) { + .fe-hero-img, + .fe-chips li, + .fe-topnav, + .fe-reveal, + .fe-jump-card, + .sidebar-logo { + animation: none !important; + transition: none !important; + opacity: 1 !important; + transform: none !important; + } +} diff --git a/docs/_static/custom.js b/docs/_static/custom.js new file mode 100644 index 0000000..4f84c90 --- /dev/null +++ b/docs/_static/custom.js @@ -0,0 +1,76 @@ +(() => { + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + function currentPath() { + try { + return decodeURIComponent(window.location.pathname); + } catch { + return window.location.pathname; + } + } + + function markHome() { + const path = currentPath(); + if ( + /\/(index\.html)?$/.test(path) || + path.endsWith("/NetForge_RL") || + path.endsWith("/NetForge_RL/") + ) { + document.documentElement.classList.add("fe-home"); + } + } + + function markNav() { + document.querySelectorAll(".fe-topnav-link").forEach((a) => { + try { + const href = new URL(a.href, window.location.href); + if (href.pathname === window.location.pathname) a.classList.add("is-current"); + } catch { + /* ignore */ + } + }); + } + + function reveal() { + const nodes = document.querySelectorAll( + ".fe-news-item, .fe-fig, .fe-card, article table, .fe-explorer" + ); + if (reduceMotion || !("IntersectionObserver" in window)) { + nodes.forEach((el) => el.classList.add("is-in")); + return; + } + const io = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) return; + entry.target.classList.add("is-in"); + io.unobserve(entry.target); + }); + }, + { rootMargin: "0px 0px -6% 0px", threshold: 0.05 } + ); + nodes.forEach((el) => { + el.classList.add("fe-reveal"); + const top = el.getBoundingClientRect().top; + if (top < window.innerHeight * 0.94) { + el.classList.add("is-in"); + } else { + io.observe(el); + } + }); + } + + function ready(fn) { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", fn, { once: true }); + } else { + fn(); + } + } + + ready(() => { + markHome(); + markNav(); + reveal(); + }); +})(); diff --git a/docs/_static/figures/arena.excalidraw b/docs/_static/figures/arena.excalidraw new file mode 100644 index 0000000..8f1521b --- /dev/null +++ b/docs/_static/figures/arena.excalidraw @@ -0,0 +1,390 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "t0103", + "type": "text", + "x": 28, + "y": 12, + "width": 280, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 400, + "version": 1, + "versionNonce": 403, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "eval splits", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "eval splits", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0104", + "type": "text", + "x": 280, + "y": 44, + "width": 620, + "height": 24, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 401, + "version": 1, + "versionNonce": 404, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "train / dev / hidden - same metrics", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "train / dev / hidden - same metrics", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0105", + "type": "rectangle", + "x": 36, + "y": 78, + "width": 350, + "height": 180, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFD166", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 446, + "version": 1, + "versionNonce": 453, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0106", + "type": "text", + "x": 36, + "y": 110, + "width": 350, + "height": 40, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 447, + "version": 1, + "versionNonce": 450, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "train", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "train", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0107", + "type": "text", + "x": 36, + "y": 160, + "width": 350, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 448, + "version": 1, + "versionNonce": 451, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "public\nprocedural packs\nlearn here", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "public\nprocedural packs\nlearn here", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0108", + "type": "rectangle", + "x": 414, + "y": 78, + "width": 350, + "height": 180, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 824, + "version": 1, + "versionNonce": 831, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0109", + "type": "text", + "x": 414, + "y": 110, + "width": 350, + "height": 40, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 825, + "version": 1, + "versionNonce": 828, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "dev", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "dev", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0110", + "type": "text", + "x": 414, + "y": 160, + "width": 350, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 826, + "version": 1, + "versionNonce": 829, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "public\nheld-out offset\npick a checkpoint", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "public\nheld-out offset\npick a checkpoint", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0111", + "type": "rectangle", + "x": 792, + "y": 78, + "width": 350, + "height": 180, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FF8FAB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1202, + "version": 1, + "versionNonce": 1209, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0112", + "type": "text", + "x": 792, + "y": 110, + "width": 350, + "height": 40, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1203, + "version": 1, + "versionNonce": 1206, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "hidden", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "hidden", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0113", + "type": "text", + "x": 792, + "y": 160, + "width": 350, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1204, + "version": 1, + "versionNonce": 1207, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "organizer\nreserved + OOD\nfinal check", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "organizer\nreserved + OOD\nfinal check", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#FFF6DC", + "currentItemFontFamily": 2, + "width": 1180, + "height": 340 + }, + "files": {} +} \ No newline at end of file diff --git a/docs/_static/figures/arena.svg b/docs/_static/figures/arena.svg new file mode 100644 index 0000000..e9517a6 --- /dev/null +++ b/docs/_static/figures/arena.svg @@ -0,0 +1,12 @@ + +eval splits + + + + +eval splits +train / dev / hidden - same metricstrainpublicprocedural packslearn here +devpublicheld-out offsetpick a checkpoint +hiddenorganizerreserved + OODfinal check +mission, SLA, false positives, kinetic - not return alone + diff --git a/docs/_static/figures/belief.excalidraw b/docs/_static/figures/belief.excalidraw new file mode 100644 index 0000000..7cd85e2 --- /dev/null +++ b/docs/_static/figures/belief.excalidraw @@ -0,0 +1,225 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "t0114", + "type": "text", + "x": 28, + "y": 12, + "width": 400, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 500, + "version": 1, + "versionNonce": 503, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SIEM vs full state", + "fontSize": 26, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "SIEM vs full state", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0115", + "type": "rectangle", + "x": 36, + "y": 78, + "width": 420, + "height": 160, + "angle": -0.002, + "strokeColor": "#2B2118", + "backgroundColor": "#FFD166", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 501, + "version": 1, + "versionNonce": 508, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0116", + "type": "text", + "x": 36, + "y": 120, + "width": 420, + "height": 80, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 502, + "version": 1, + "versionNonce": 505, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SIEM belief\nwhat a SOC would see", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "SIEM belief\nwhat a SOC would see", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0117", + "type": "arrow", + "x": 456, + "y": 158, + "width": 78, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 503, + "version": 1, + "versionNonce": 512, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 78, + 0 + ] + ] + }, + { + "id": "r0118", + "type": "rectangle", + "x": 534, + "y": 78, + "width": 410, + "height": 160, + "angle": 0.002, + "strokeColor": "#2B2118", + "backgroundColor": "#FF8FAB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 504, + "version": 1, + "versionNonce": 511, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0119", + "type": "text", + "x": 534, + "y": 120, + "width": 410, + "height": 80, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 505, + "version": 1, + "versionNonce": 508, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "oracle graph\ntrue compromise, debug only", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "oracle graph\ntrue compromise, debug only", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#FFF6DC", + "currentItemFontFamily": 2, + "width": 980, + "height": 300 + }, + "files": {} +} \ No newline at end of file diff --git a/docs/_static/figures/belief.svg b/docs/_static/figures/belief.svg new file mode 100644 index 0000000..9c2f5e6 --- /dev/null +++ b/docs/_static/figures/belief.svg @@ -0,0 +1,12 @@ + +SIEM vs full state + + + + +SIEM vs full state +Blue trains on logs - full map is diagnosticsSIEM beliefwhat a SOC would seepartial, delayed, noisydefault for Blue + +oracle graphtrue compromisedebug / replaynot a Blue obs +netforge questions belief-vs-oracle + diff --git a/docs/_static/figures/build.py b/docs/_static/figures/build.py new file mode 100644 index 0000000..3d98970 --- /dev/null +++ b/docs/_static/figures/build.py @@ -0,0 +1,547 @@ +"""Sticky-note SVGs + Excalidraw sources (FlowEdge visual language).""" + +from __future__ import annotations + +import json +from pathlib import Path + +OUT = Path(__file__).resolve().parent + +INK = "#2B2118" +TITLE = "#1B4B6B" +CANVAS = "#FFF6DC" +EDGE = "#E8D7A8" +SHADOW = "#E8C98A" +YEL = "#FFD166" +TEAL = "#2EC4B6" +PINK = "#FF8FAB" +ORANGE = "#FF9F43" +PURPLE = "#7B6CF6" +GREEN = "#6BCB77" +CREAM = "#FFF1C2" +BLUE = "#8EC5FF" +RED = "#FF6B6B" +FONT = "Trebuchet MS, Segoe UI, sans-serif" + + +def header(title: str, w: int, h: int, mid: str | None = None) -> str: + extra = "" + if mid: + extra = ( + f'{mid}' + ) + return f''' +{title} + + + + +{title} +{extra}''' + + +def _slug(title: str) -> str: + return "".join(ch if ch.isalnum() else "-" for ch in title.lower()).strip("-")[:24] + + +def close() -> str: + return "\n" + + +def card(x: float, y: float, w: float, h: float, fill: str, rot: float, inner: str) -> str: + cx = x + w / 2 + cy = y + h / 2 + return ( + f'' + f'' + f'' + f"{inner}\n" + ) + + +def xml_escape(text: str) -> str: + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +def t(x: float, y: float, size: int, weight: str, fill: str, text: str, anchor: str = "middle") -> str: + return ( + f'{xml_escape(text)}' + ) + + +def chip(x: float, y: float, w: float, h: float, fill: str, rot: float, label: str) -> str: + cx = x + w / 2 + cy = y + h / 2 + return ( + f'' + f'' + f'{t(cx, y + 24, 14, "800", INK, label)}\n' + ) + + +def arrow(x1: float, y1: float, x2: float, y2: float, title: str, qy: float | None = None) -> str: + mid = (x1 + x2) / 2 + q = y1 if qy is None else qy + return ( + f'\n' + ) + + +def write(name: str, body: str) -> None: + path = OUT / name + path.write_text(body, encoding="utf-8") + print("wrote", path.name, path.stat().st_size) + + +# --------------------------------------------------------------------------- +# Excalidraw +# --------------------------------------------------------------------------- + +_EID = 0 + + +def eid(prefix: str = "e") -> str: + global _EID + _EID += 1 + return f"{prefix}{_EID:04d}" + + +def e_rect(x, y, w, h, bg, angle=0.0, seed=1): + return { + "id": eid("r"), + "type": "rectangle", + "x": x, + "y": y, + "width": w, + "height": h, + "angle": angle, + "strokeColor": INK, + "backgroundColor": bg, + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": None, + "roundness": {"type": 3}, + "seed": seed, + "version": 1, + "versionNonce": seed + 7, + "isDeleted": False, + "boundElements": None, + "updated": 1, + "link": None, + "locked": False, + } + + +def e_text(x, y, w, h, text, color=INK, size=20, align="center", seed=2): + return { + "id": eid("t"), + "type": "text", + "x": x, + "y": y, + "width": w, + "height": h, + "angle": 0, + "strokeColor": color, + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": None, + "roundness": None, + "seed": seed, + "version": 1, + "versionNonce": seed + 3, + "isDeleted": False, + "boundElements": None, + "updated": 1, + "link": None, + "locked": False, + "text": text, + "fontSize": size, + "fontFamily": 2, + "textAlign": align, + "verticalAlign": "middle", + "containerId": None, + "originalText": text, + "autoResize": True, + "lineHeight": 1.25, + } + + +def e_arrow(x, y, w, h, seed=3): + return { + "id": eid("a"), + "type": "arrow", + "x": x, + "y": y, + "width": w, + "height": h, + "angle": 0, + "strokeColor": TITLE, + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": None, + "roundness": {"type": 2}, + "seed": seed, + "version": 1, + "versionNonce": seed + 9, + "isDeleted": False, + "boundElements": None, + "updated": 1, + "link": None, + "locked": False, + "startBinding": None, + "endBinding": None, + "lastCommittedPoint": None, + "startArrowhead": None, + "endArrowhead": "arrow", + "points": [[0, 0], [w, h]], + } + + +def scene(elements: list, w: int = 1180, h: int = 400) -> dict: + return { + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": elements, + "appState": { + "gridSize": None, + "viewBackgroundColor": CANVAS, + "currentItemFontFamily": 2, + "width": w, + "height": h, + }, + "files": {}, + } + + +def write_ex(name: str, elements: list, w: int = 1180, h: int = 400) -> None: + path = OUT / name + path.write_text(json.dumps(scene(elements, w, h), indent=2), encoding="utf-8") + print("wrote", path.name, path.stat().st_size) + + +# --------------------------------------------------------------------------- +# Diagrams +# --------------------------------------------------------------------------- + +def mark_svg() -> str: + return f''' +NetForge + + + + + + + + +''' + + +def purpose(): + title = "the match" + parts = [header(title, 1180, 400, "Red attacks - Blue defends - SIEM in the middle")] + parts.append( + card( + 36, 78, 350, 220, RED, -0.08, + t(211, 156, 24, "800", TITLE, "Red") + + t(211, 188, 16, "600", INK, "discover, exploit") + + t(211, 208, 16, "600", INK, "escalate, exfil") + + t(211, 228, 16, "600", INK, "fog of war") + + t(211, 266, 14, "600", TITLE, "starts blind"), + ) + ) + parts.append( + card( + 414, 78, 350, 220, TEAL, 0.24, + t(589, 156, 24, "800", TITLE, "Network") + + t(589, 188, 16, "600", INK, "hosts, subnets") + + t(589, 208, 16, "600", INK, "firewalls, decoys") + + t(589, 228, 16, "600", INK, "durative actions") + + t(589, 266, 14, "600", TITLE, "the gym world"), + ) + ) + parts.append( + card( + 792, 78, 350, 220, BLUE, 0.10, + t(967, 156, 24, "800", TITLE, "Blue") + + t(967, 188, 16, "600", INK, "isolate, restore") + + t(967, 208, 16, "600", INK, "decoys, EDR") + + t(967, 228, 16, "600", INK, "reads SIEM only") + + t(967, 266, 14, "600", TITLE, "no oracle"), + ) + ) + parts.append(chip(70, 312, 148, 36, YEL, 0.9, "ransomware")) + parts.append(chip(470, 312, 174, 36, ORANGE, -1.4, "APT / cloud / OT")) + parts.append(chip(848, 312, 174, 36, PINK, -1.4, "1 Red + 3 Blue")) + parts.append(t(590, 378, 15, "800", TITLE, "PettingZoo parallel -> or Gymnasium Blue-v4")) + parts.append(close()) + write("purpose.svg", "".join(parts)) + write_ex( + "purpose.excalidraw", + [ + e_text(28, 12, 280, 32, "the match", TITLE, 28, "left", 11), + e_text(240, 44, 700, 24, "Red attacks - Blue defends - SIEM in the middle", TITLE, 18, "center", 12), + e_rect(36, 78, 350, 220, RED, -0.01, 21), + e_text(36, 150, 350, 40, "Red", TITLE, 28, "center", 22), + e_text(36, 190, 350, 80, "discover, exploit\nescalate, exfil\nstarts blind", INK, 18, "center", 23), + e_rect(414, 78, 350, 220, TEAL, 0.004, 31), + e_text(414, 150, 350, 40, "Network", TITLE, 28, "center", 32), + e_text(414, 190, 350, 80, "hosts, subnets\nfirewalls, decoys\ndurative actions", INK, 18, "center", 33), + e_rect(792, 78, 350, 220, BLUE, 0.002, 41), + e_text(792, 150, 350, 40, "Blue", TITLE, 28, "center", 42), + e_text(792, 190, 350, 80, "isolate, restore\nreads SIEM only\nno oracle", INK, 18, "center", 43), + ], + 1180, + 400, + ) + + +def workflow(): + title = "one tick" + parts = [header(title, 980, 270)] + boxes = [ + (24, 78, 155, 100, YEL, -0.13, "reset", "seeded net"), + (210, 78, 165, 100, TEAL, -0.07, "observe", "SIEM + mask"), + (406, 78, 185, 100, PURPLE, -0.15, "act", "Red / Blue"), + (622, 78, 155, 100, ORANGE, -0.25, "resolve", "conflicts"), + (808, 78, 148, 100, GREEN, -0.17, "log", "Sysmon-like"), + ] + for x, y, w, h, fill, rot, a, b in boxes: + cx = x + w / 2 + parts.append( + card(x, y, w, h, fill, rot, t(cx, 118, 16, "800", INK, a) + t(cx, 140, 14, "600", INK, b)) + ) + parts.append(arrow(179, 128, 210, 128, title, 133)) + parts.append(arrow(375, 128, 406, 128, title, 126)) + parts.append(arrow(591, 128, 622, 128, title, 128)) + parts.append(arrow(777, 128, 808, 128, title, 129)) + parts.append(chip(220, 200, 527, 36, BLUE, -1.5, "exploits and isolates take time - logs can be late")) + parts.append(close()) + write("workflow.svg", "".join(parts)) + els = [e_text(28, 12, 280, 32, "one tick", TITLE, 28, "left", 61)] + xs = [24, 210, 406, 622, 808] + fills = [YEL, TEAL, PURPLE, ORANGE, GREEN] + labels = [("reset", "seeded net"), ("observe", "SIEM + mask"), ("act", "Red / Blue"), ("resolve", "conflicts"), ("log", "Sysmon-like")] + ws = [155, 165, 185, 155, 148] + for i, (x, fill, (a, b), w) in enumerate(zip(xs, fills, labels, ws)): + els += [ + e_rect(x, 78, w, 100, fill, 0, 70 + i), + e_text(x, 100, w, 28, a, INK, 20, "center", 80 + i), + e_text(x, 132, w, 24, b, INK, 16, "center", 90 + i), + ] + if i: + els.append(e_arrow(xs[i - 1] + ws[i - 1], 128, x - (xs[i - 1] + ws[i - 1]), 0, 100 + i)) + els += [ + e_rect(220, 200, 527, 36, BLUE, -0.026, 110), + e_text(220, 202, 527, 32, "exploits and isolates take time - logs can be late", INK, 16, "center", 111), + ] + write_ex("workflow.excalidraw", els, 980, 270) + + +def why_this(): + title = "what Blue actually sees" + rows = [ + (YEL, "true compromise map", "simulator state", TEAL, "SIEM belief graph", "alerts + inventory"), + (PINK, "instant telemetry", "every exploit is obvious", GREEN, "delayed / noisy logs", "log_latency, drop, decoys"), + (ORANGE, "one giant isolate", "kill the network to win", PURPLE, "SLA vs security", "keep hosts online"), + (RED, "scripted campaign only", "one attacker forever", TEAL, "Red population", "random / heuristic / kill-chain"), + (PINK, "discrete instant acts", "done in one step", GREEN, "durative actions", "ticks until complete"), + ] + parts = [header(title, 980, 570)] + y = 72 + for i, (lc, lt, ls, rc, rt, rs) in enumerate(rows): + parts.append( + card( + 36, y, 380, 70, lc, 0.04 if i % 2 == 0 else -0.12, + t(226, y + 25, 16, "800", INK, lt) + t(226, y + 47, 14, "600", INK, ls), + ) + ) + parts.append(arrow(428, y + 35, 534, y + 35, title, y + 32)) + parts.append( + card( + 544, y, 400, 70, rc, -0.08 if i % 2 == 0 else 0.06, + t(744, y + 25, 16, "800", INK, rt) + t(744, y + 47, 14, "600", INK, rs), + ) + ) + y += 78 + parts.append(chip(36, 472, 180, 36, CREAM, -1.4, "SOC view")) + parts.append(chip(248, 472, 200, 36, CREAM, -0.8, "not god-mode")) + parts.append(chip(532, 472, 226, 36, YEL, -1.2, "keep the lights on")) + parts.append(t(490, 540, 14, "800", TITLE, "Blue trains on logs - Red trains on recon - the rest is hidden")) + parts.append(close()) + write("why-this.svg", "".join(parts)) + els = [e_text(28, 12, 520, 32, "what Blue actually sees", TITLE, 26, "left", 200)] + y = 72 + for i, (lc, lt, ls, rc, rt, rs) in enumerate(rows): + els += [ + e_rect(36, y, 380, 70, lc, 0, 210 + i), + e_text(36, y + 8, 380, 28, lt, INK, 18, "center", 220 + i), + e_text(36, y + 36, 380, 24, ls, INK, 14, "center", 230 + i), + e_arrow(416, y + 35, 128, 0, 240 + i), + e_rect(544, y, 400, 70, rc, 0, 250 + i), + e_text(544, y + 8, 400, 28, rt, INK, 18, "center", 260 + i), + e_text(544, y + 36, 400, 24, rs, INK, 14, "center", 270 + i), + ] + y += 78 + write_ex("why-this.excalidraw", els, 980, 570) + + +def status(): + title = "in the gym" + parts = [header(title, 980, 520)] + + def pill(x: float, y: float, w: float, fill: str, mark: str, name: str, rot: float) -> None: + parts.append(card(x, y, w, 56, fill, rot, "")) + parts.append( + f'' + ) + parts.append(t(x + 18, y + 33, 13, "800", INK, mark)) + parts.append(t(x + w / 2 + 10, y + 32, 13, "800", TITLE, name)) + + parts.append(t(36, 68, 16, "800", TITLE, "APIs", "start")) + pill(36, 80, 220, GREEN, "+", "PettingZoo", 0.11) + pill(268, 80, 250, GREEN, "+", "Gymnasium Blue-v4", 0.15) + pill(530, 80, 200, GREEN, "+", "AEC wrapper", 0.08) + + parts.append(t(36, 160, 16, "800", TITLE, "World", "start")) + pill(36, 172, 200, GREEN, "+", "SIEM logs", 0.25) + pill(248, 172, 210, GREEN, "+", "belief graphs", 0.06) + pill(470, 172, 200, GREEN, "+", "decoys / OT", -0.05) + pill(682, 172, 190, TEAL, "+", "HTML replay", -0.20) + + parts.append(t(36, 252, 16, "800", TITLE, "Agents", "start")) + pill(36, 264, 200, GREEN, "+", "heuristics", 0.14) + pill(248, 264, 220, GREEN, "+", "kill-chain Red", 0.02) + pill(480, 264, 210, TEAL, "+", "scripted Blue", -0.11) + + parts.append(t(36, 344, 16, "800", TITLE, "Your code", "start")) + pill(36, 356, 160, CREAM, "x", "MAPPO", 0.11) + pill(208, 356, 150, CREAM, "x", "QMIX", -0.21) + pill(370, 356, 190, CREAM, "x", "GNN trainer", -0.18) + pill(572, 356, 200, CREAM, "x", "LLM agent", -0.08) + + parts.append( + t(36, 448, 14, "800", TITLE, "entry: gym.make / pettingzoo.make - netforge run / evaluate", "start") + ) + parts.append(t(36, 478, 14, "800", TITLE, "+ ships in this repo x train elsewhere, run here", "start")) + parts.append(close()) + write("status.svg", "".join(parts)) + els = [e_text(28, 12, 280, 32, "in the gym", TITLE, 26, "left", 300)] + rows_e = [ + (80, "APIs", [(36, 220, "PettingZoo", GREEN), (268, 250, "Gymnasium Blue-v4", GREEN), (530, 200, "AEC wrapper", GREEN)]), + (172, "World", [(36, 200, "SIEM logs", GREEN), (248, 210, "belief graphs", GREEN), (470, 200, "decoys / OT", GREEN), (682, 190, "HTML replay", TEAL)]), + (264, "Agents", [(36, 200, "heuristics", GREEN), (248, 220, "kill-chain Red", GREEN), (480, 210, "scripted Blue", TEAL)]), + (356, "Your code", [(36, 160, "MAPPO", CREAM), (208, 150, "QMIX", CREAM), (370, 190, "GNN trainer", CREAM), (572, 200, "LLM agent", CREAM)]), + ] + for y, lab, items in rows_e: + els.append(e_text(36, y - 28, 200, 24, lab, TITLE, 18, "left", y)) + for x, w, name, fill in items: + els += [e_rect(x, y, w, 56, fill, 0, y + x), e_text(x, y + 12, w, 32, name, TITLE, 16, "center", y + x + 1)] + write_ex("status.excalidraw", els, 980, 520) + + +def arena(): + title = "eval splits" + parts = [header(title, 1180, 340, "train / dev / hidden - same metrics")] + cols = [ + (36, YEL, "train", "public", "procedural packs", "learn here"), + (414, TEAL, "dev", "public", "held-out offset", "pick a checkpoint"), + (792, PINK, "hidden", "organizer", "reserved + OOD", "final check"), + ] + for x, fill, name, who, how, use in cols: + cx = x + 175 + parts.append( + card( + x, 78, 350, 180, fill, 0.12 if fill == YEL else (-0.1 if fill == TEAL else 0.06), + t(cx, 130, 28, "800", TITLE, name) + + t(cx, 168, 16, "600", INK, who) + + t(cx, 192, 16, "600", INK, how) + + t(cx, 228, 14, "600", TITLE, use), + ) + ) + parts.append(chip(200, 274, 780, 36, BLUE, -0.7, "mission, SLA, false positives, kinetic - not return alone")) + parts.append(close()) + write("arena.svg", "".join(parts)) + els = [ + e_text(28, 12, 280, 32, "eval splits", TITLE, 28, "left", 400), + e_text(280, 44, 620, 24, "train / dev / hidden - same metrics", TITLE, 18, "center", 401), + ] + for x, fill, name, who, how, use in cols: + els += [ + e_rect(x, 78, 350, 180, fill, 0, 410 + x), + e_text(x, 110, 350, 40, name, TITLE, 28, "center", 411 + x), + e_text(x, 160, 350, 70, f"{who}\n{how}\n{use}", INK, 18, "center", 412 + x), + ] + write_ex("arena.excalidraw", els, 1180, 340) + + +def belief(): + title = "SIEM vs full state" + parts = [header(title, 980, 300, "Blue trains on logs - full map is diagnostics")] + parts.append( + card( + 36, 78, 420, 160, YEL, -0.12, + t(246, 130, 22, "800", TITLE, "SIEM belief") + + t(246, 162, 16, "600", INK, "what a SOC would see") + + t(246, 186, 16, "600", INK, "partial, delayed, noisy") + + t(246, 214, 14, "600", TITLE, "default for Blue"), + ) + ) + parts.append(arrow(468, 158, 524, 158, title, 150)) + parts.append( + card( + 534, 78, 410, 160, PINK, 0.14, + t(739, 130, 22, "800", TITLE, "oracle graph") + + t(739, 162, 16, "600", INK, "true compromise") + + t(739, 186, 16, "600", INK, "debug / replay") + + t(739, 214, 14, "600", TITLE, "not a Blue obs"), + ) + ) + parts.append(chip(220, 250, 540, 36, TEAL, -1.1, "netforge questions belief-vs-oracle")) + parts.append(close()) + write("belief.svg", "".join(parts)) + write_ex( + "belief.excalidraw", + [ + e_text(28, 12, 400, 32, "SIEM vs full state", TITLE, 26, "left", 500), + e_rect(36, 78, 420, 160, YEL, -0.002, 501), + e_text(36, 120, 420, 80, "SIEM belief\nwhat a SOC would see", INK, 20, "center", 502), + e_arrow(456, 158, 78, 0, 503), + e_rect(534, 78, 410, 160, PINK, 0.002, 504), + e_text(534, 120, 410, 80, "oracle graph\ntrue compromise, debug only", INK, 20, "center", 505), + ], + 980, + 300, + ) + +def main() -> None: + (OUT / 'mark.svg').write_text(mark_svg(), encoding='utf-8') + print('wrote mark.svg') + purpose() + workflow() + why_this() + status() + arena() + belief() + + +if __name__ == '__main__': + main() diff --git a/docs/_static/figures/mark.svg b/docs/_static/figures/mark.svg new file mode 100644 index 0000000..20457b5 --- /dev/null +++ b/docs/_static/figures/mark.svg @@ -0,0 +1,10 @@ + +NetForge + + + + + + + + diff --git a/docs/_static/figures/purpose.excalidraw b/docs/_static/figures/purpose.excalidraw new file mode 100644 index 0000000..f613397 --- /dev/null +++ b/docs/_static/figures/purpose.excalidraw @@ -0,0 +1,390 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "t0001", + "type": "text", + "x": 28, + "y": 12, + "width": 280, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 11, + "version": 1, + "versionNonce": 14, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "the match", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "the match", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0002", + "type": "text", + "x": 240, + "y": 44, + "width": 700, + "height": 24, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 12, + "version": 1, + "versionNonce": 15, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Red attacks - Blue defends - SIEM in the middle", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Red attacks - Blue defends - SIEM in the middle", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0003", + "type": "rectangle", + "x": 36, + "y": 78, + "width": 350, + "height": 220, + "angle": -0.01, + "strokeColor": "#2B2118", + "backgroundColor": "#FF6B6B", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 21, + "version": 1, + "versionNonce": 28, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0004", + "type": "text", + "x": 36, + "y": 150, + "width": 350, + "height": 40, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 22, + "version": 1, + "versionNonce": 25, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Red", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Red", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0005", + "type": "text", + "x": 36, + "y": 190, + "width": 350, + "height": 80, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 23, + "version": 1, + "versionNonce": 26, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "discover, exploit\nescalate, exfil\nstarts blind", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "discover, exploit\nescalate, exfil\nstarts blind", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0006", + "type": "rectangle", + "x": 414, + "y": 78, + "width": 350, + "height": 220, + "angle": 0.004, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 31, + "version": 1, + "versionNonce": 38, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0007", + "type": "text", + "x": 414, + "y": 150, + "width": 350, + "height": 40, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 32, + "version": 1, + "versionNonce": 35, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Network", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Network", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0008", + "type": "text", + "x": 414, + "y": 190, + "width": 350, + "height": 80, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 33, + "version": 1, + "versionNonce": 36, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "hosts, subnets\nfirewalls, decoys\ndurative actions", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "hosts, subnets\nfirewalls, decoys\ndurative actions", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0009", + "type": "rectangle", + "x": 792, + "y": 78, + "width": 350, + "height": 220, + "angle": 0.002, + "strokeColor": "#2B2118", + "backgroundColor": "#8EC5FF", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 41, + "version": 1, + "versionNonce": 48, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0010", + "type": "text", + "x": 792, + "y": 150, + "width": 350, + "height": 40, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 42, + "version": 1, + "versionNonce": 45, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Blue", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Blue", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0011", + "type": "text", + "x": 792, + "y": 190, + "width": 350, + "height": 80, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 43, + "version": 1, + "versionNonce": 46, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "isolate, restore\nreads SIEM only\nno oracle", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "isolate, restore\nreads SIEM only\nno oracle", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#FFF6DC", + "currentItemFontFamily": 2, + "width": 1180, + "height": 400 + }, + "files": {} +} \ No newline at end of file diff --git a/docs/_static/figures/purpose.svg b/docs/_static/figures/purpose.svg new file mode 100644 index 0000000..ada515a --- /dev/null +++ b/docs/_static/figures/purpose.svg @@ -0,0 +1,14 @@ + +the match + + + + +the match +Red attacks - Blue defends - SIEM in the middleReddiscover, exploitescalate, exfilfog of warstarts blind +Networkhosts, subnetsfirewalls, decoysdurative actionsthe gym world +Blueisolate, restoredecoys, EDRreads SIEM onlyno oracle +ransomware +APT / cloud / OT +1 Red + 3 Blue +PettingZoo parallel -> or Gymnasium Blue-v4 diff --git a/docs/_static/figures/status.excalidraw b/docs/_static/figures/status.excalidraw new file mode 100644 index 0000000..2aa0dd9 --- /dev/null +++ b/docs/_static/figures/status.excalidraw @@ -0,0 +1,1105 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "t0070", + "type": "text", + "x": 28, + "y": 12, + "width": 280, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 300, + "version": 1, + "versionNonce": 303, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "in the gym", + "fontSize": 26, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "in the gym", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0071", + "type": "text", + "x": 36, + "y": 52, + "width": 200, + "height": 24, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 80, + "version": 1, + "versionNonce": 83, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "APIs", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "APIs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0072", + "type": "rectangle", + "x": 36, + "y": 80, + "width": 220, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 116, + "version": 1, + "versionNonce": 123, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0073", + "type": "text", + "x": 36, + "y": 92, + "width": 220, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 117, + "version": 1, + "versionNonce": 120, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "PettingZoo", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "PettingZoo", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0074", + "type": "rectangle", + "x": 268, + "y": 80, + "width": 250, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 348, + "version": 1, + "versionNonce": 355, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0075", + "type": "text", + "x": 268, + "y": 92, + "width": 250, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 349, + "version": 1, + "versionNonce": 352, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Gymnasium Blue-v4", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Gymnasium Blue-v4", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0076", + "type": "rectangle", + "x": 530, + "y": 80, + "width": 200, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 610, + "version": 1, + "versionNonce": 617, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0077", + "type": "text", + "x": 530, + "y": 92, + "width": 200, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 611, + "version": 1, + "versionNonce": 614, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "AEC wrapper", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "AEC wrapper", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0078", + "type": "text", + "x": 36, + "y": 144, + "width": 200, + "height": 24, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 172, + "version": 1, + "versionNonce": 175, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "World", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "World", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0079", + "type": "rectangle", + "x": 36, + "y": 172, + "width": 200, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 208, + "version": 1, + "versionNonce": 215, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0080", + "type": "text", + "x": 36, + "y": 184, + "width": 200, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 209, + "version": 1, + "versionNonce": 212, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SIEM logs", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "SIEM logs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0081", + "type": "rectangle", + "x": 248, + "y": 172, + "width": 210, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 420, + "version": 1, + "versionNonce": 427, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0082", + "type": "text", + "x": 248, + "y": 184, + "width": 210, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 421, + "version": 1, + "versionNonce": 424, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "belief graphs", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "belief graphs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0083", + "type": "rectangle", + "x": 470, + "y": 172, + "width": 200, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 642, + "version": 1, + "versionNonce": 649, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0084", + "type": "text", + "x": 470, + "y": 184, + "width": 200, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 643, + "version": 1, + "versionNonce": 646, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "decoys / OT", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "decoys / OT", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0085", + "type": "rectangle", + "x": 682, + "y": 172, + "width": 190, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 854, + "version": 1, + "versionNonce": 861, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0086", + "type": "text", + "x": 682, + "y": 184, + "width": 190, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 855, + "version": 1, + "versionNonce": 858, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "HTML replay", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "HTML replay", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0087", + "type": "text", + "x": 36, + "y": 236, + "width": 200, + "height": 24, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 264, + "version": 1, + "versionNonce": 267, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Agents", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Agents", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0088", + "type": "rectangle", + "x": 36, + "y": 264, + "width": 200, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 300, + "version": 1, + "versionNonce": 307, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0089", + "type": "text", + "x": 36, + "y": 276, + "width": 200, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 301, + "version": 1, + "versionNonce": 304, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "heuristics", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "heuristics", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0090", + "type": "rectangle", + "x": 248, + "y": 264, + "width": 220, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 512, + "version": 1, + "versionNonce": 519, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0091", + "type": "text", + "x": 248, + "y": 276, + "width": 220, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 513, + "version": 1, + "versionNonce": 516, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "kill-chain Red", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "kill-chain Red", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0092", + "type": "rectangle", + "x": 480, + "y": 264, + "width": 210, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 744, + "version": 1, + "versionNonce": 751, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0093", + "type": "text", + "x": 480, + "y": 276, + "width": 210, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 745, + "version": 1, + "versionNonce": 748, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "scripted Blue", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "scripted Blue", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0094", + "type": "text", + "x": 36, + "y": 328, + "width": 200, + "height": 24, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 356, + "version": 1, + "versionNonce": 359, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Your code", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Your code", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0095", + "type": "rectangle", + "x": 36, + "y": 356, + "width": 160, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFF1C2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 392, + "version": 1, + "versionNonce": 399, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0096", + "type": "text", + "x": 36, + "y": 368, + "width": 160, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 393, + "version": 1, + "versionNonce": 396, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "MAPPO", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "MAPPO", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0097", + "type": "rectangle", + "x": 208, + "y": 356, + "width": 150, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFF1C2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 564, + "version": 1, + "versionNonce": 571, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0098", + "type": "text", + "x": 208, + "y": 368, + "width": 150, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 565, + "version": 1, + "versionNonce": 568, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "QMIX", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "QMIX", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0099", + "type": "rectangle", + "x": 370, + "y": 356, + "width": 190, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFF1C2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 726, + "version": 1, + "versionNonce": 733, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0100", + "type": "text", + "x": 370, + "y": 368, + "width": 190, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 727, + "version": 1, + "versionNonce": 730, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "GNN trainer", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "GNN trainer", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0101", + "type": "rectangle", + "x": 572, + "y": 356, + "width": 200, + "height": 56, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFF1C2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 928, + "version": 1, + "versionNonce": 935, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0102", + "type": "text", + "x": 572, + "y": 368, + "width": 200, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 929, + "version": 1, + "versionNonce": 932, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "LLM agent", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "LLM agent", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#FFF6DC", + "currentItemFontFamily": 2, + "width": 980, + "height": 520 + }, + "files": {} +} \ No newline at end of file diff --git a/docs/_static/figures/status.svg b/docs/_static/figures/status.svg new file mode 100644 index 0000000..febacbf --- /dev/null +++ b/docs/_static/figures/status.svg @@ -0,0 +1,22 @@ + +in the gym + + + + +in the gym +APIs ++PettingZoo ++Gymnasium Blue-v4 ++AEC wrapperWorld ++SIEM logs ++belief graphs ++decoys / OT ++HTML replayAgents ++heuristics ++kill-chain Red ++scripted BlueYour code +xMAPPO +xQMIX +xGNN trainer +xLLM agententry: gym.make / pettingzoo.make - netforge run / evaluate+ ships in this repo x train elsewhere, run here diff --git a/docs/_static/figures/why-this.excalidraw b/docs/_static/figures/why-this.excalidraw new file mode 100644 index 0000000..b76964c --- /dev/null +++ b/docs/_static/figures/why-this.excalidraw @@ -0,0 +1,1281 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "t0034", + "type": "text", + "x": 28, + "y": 12, + "width": 520, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 200, + "version": 1, + "versionNonce": 203, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "what Blue actually sees", + "fontSize": 26, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "what Blue actually sees", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0035", + "type": "rectangle", + "x": 36, + "y": 72, + "width": 380, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFD166", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 210, + "version": 1, + "versionNonce": 217, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0036", + "type": "text", + "x": 36, + "y": 80, + "width": 380, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 220, + "version": 1, + "versionNonce": 223, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "true compromise map", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "true compromise map", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0037", + "type": "text", + "x": 36, + "y": 108, + "width": 380, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 230, + "version": 1, + "versionNonce": 233, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "simulator state", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "simulator state", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0038", + "type": "arrow", + "x": 416, + "y": 107, + "width": 128, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 240, + "version": 1, + "versionNonce": 249, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 128, + 0 + ] + ] + }, + { + "id": "r0039", + "type": "rectangle", + "x": 544, + "y": 72, + "width": 400, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 250, + "version": 1, + "versionNonce": 257, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0040", + "type": "text", + "x": 544, + "y": 80, + "width": 400, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 260, + "version": 1, + "versionNonce": 263, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SIEM belief graph", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "SIEM belief graph", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0041", + "type": "text", + "x": 544, + "y": 108, + "width": 400, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 270, + "version": 1, + "versionNonce": 273, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "alerts + inventory", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "alerts + inventory", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0042", + "type": "rectangle", + "x": 36, + "y": 150, + "width": 380, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FF8FAB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 211, + "version": 1, + "versionNonce": 218, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0043", + "type": "text", + "x": 36, + "y": 158, + "width": 380, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 221, + "version": 1, + "versionNonce": 224, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "instant telemetry", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "instant telemetry", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0044", + "type": "text", + "x": 36, + "y": 186, + "width": 380, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 231, + "version": 1, + "versionNonce": 234, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "every exploit is obvious", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "every exploit is obvious", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0045", + "type": "arrow", + "x": 416, + "y": 185, + "width": 128, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 241, + "version": 1, + "versionNonce": 250, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 128, + 0 + ] + ] + }, + { + "id": "r0046", + "type": "rectangle", + "x": 544, + "y": 150, + "width": 400, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 251, + "version": 1, + "versionNonce": 258, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0047", + "type": "text", + "x": 544, + "y": 158, + "width": 400, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 261, + "version": 1, + "versionNonce": 264, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "delayed / noisy logs", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "delayed / noisy logs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0048", + "type": "text", + "x": 544, + "y": 186, + "width": 400, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 271, + "version": 1, + "versionNonce": 274, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "log_latency, drop, decoys", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "log_latency, drop, decoys", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0049", + "type": "rectangle", + "x": 36, + "y": 228, + "width": 380, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FF9F43", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 212, + "version": 1, + "versionNonce": 219, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0050", + "type": "text", + "x": 36, + "y": 236, + "width": 380, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 222, + "version": 1, + "versionNonce": 225, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "one giant isolate", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "one giant isolate", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0051", + "type": "text", + "x": 36, + "y": 264, + "width": 380, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 232, + "version": 1, + "versionNonce": 235, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "kill the network to win", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "kill the network to win", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0052", + "type": "arrow", + "x": 416, + "y": 263, + "width": 128, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 242, + "version": 1, + "versionNonce": 251, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 128, + 0 + ] + ] + }, + { + "id": "r0053", + "type": "rectangle", + "x": 544, + "y": 228, + "width": 400, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#7B6CF6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 252, + "version": 1, + "versionNonce": 259, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0054", + "type": "text", + "x": 544, + "y": 236, + "width": 400, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 262, + "version": 1, + "versionNonce": 265, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SLA vs security", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "SLA vs security", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0055", + "type": "text", + "x": 544, + "y": 264, + "width": 400, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 272, + "version": 1, + "versionNonce": 275, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "keep hosts online", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "keep hosts online", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0056", + "type": "rectangle", + "x": 36, + "y": 306, + "width": 380, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FF6B6B", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 213, + "version": 1, + "versionNonce": 220, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0057", + "type": "text", + "x": 36, + "y": 314, + "width": 380, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 223, + "version": 1, + "versionNonce": 226, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "scripted campaign only", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "scripted campaign only", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0058", + "type": "text", + "x": 36, + "y": 342, + "width": 380, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 233, + "version": 1, + "versionNonce": 236, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "one attacker forever", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "one attacker forever", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0059", + "type": "arrow", + "x": 416, + "y": 341, + "width": 128, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 243, + "version": 1, + "versionNonce": 252, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 128, + 0 + ] + ] + }, + { + "id": "r0060", + "type": "rectangle", + "x": 544, + "y": 306, + "width": 400, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 253, + "version": 1, + "versionNonce": 260, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0061", + "type": "text", + "x": 544, + "y": 314, + "width": 400, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 263, + "version": 1, + "versionNonce": 266, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Red population", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Red population", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0062", + "type": "text", + "x": 544, + "y": 342, + "width": 400, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 273, + "version": 1, + "versionNonce": 276, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "random / heuristic / kill-chain", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "random / heuristic / kill-chain", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0063", + "type": "rectangle", + "x": 36, + "y": 384, + "width": 380, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FF8FAB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 214, + "version": 1, + "versionNonce": 221, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0064", + "type": "text", + "x": 36, + "y": 392, + "width": 380, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 224, + "version": 1, + "versionNonce": 227, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "discrete instant acts", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "discrete instant acts", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0065", + "type": "text", + "x": 36, + "y": 420, + "width": 380, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 234, + "version": 1, + "versionNonce": 237, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "done in one step", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "done in one step", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0066", + "type": "arrow", + "x": 416, + "y": 419, + "width": 128, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 244, + "version": 1, + "versionNonce": 253, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 128, + 0 + ] + ] + }, + { + "id": "r0067", + "type": "rectangle", + "x": 544, + "y": 384, + "width": 400, + "height": 70, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 254, + "version": 1, + "versionNonce": 261, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0068", + "type": "text", + "x": 544, + "y": 392, + "width": 400, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 264, + "version": 1, + "versionNonce": 267, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "durative actions", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "durative actions", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0069", + "type": "text", + "x": 544, + "y": 420, + "width": 400, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 274, + "version": 1, + "versionNonce": 277, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "ticks until complete", + "fontSize": 14, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "ticks until complete", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#FFF6DC", + "currentItemFontFamily": 2, + "width": 980, + "height": 570 + }, + "files": {} +} \ No newline at end of file diff --git a/docs/_static/figures/why-this.svg b/docs/_static/figures/why-this.svg new file mode 100644 index 0000000..7abe2b8 --- /dev/null +++ b/docs/_static/figures/why-this.svg @@ -0,0 +1,26 @@ + +what Blue actually sees + + + + +what Blue actually sees +true compromise mapsimulator state + +SIEM belief graphalerts + inventory +instant telemetryevery exploit is obvious + +delayed / noisy logslog_latency, drop, decoys +one giant isolatekill the network to win + +SLA vs securitykeep hosts online +scripted campaign onlyone attacker forever + +Red populationrandom / heuristic / kill-chain +discrete instant actsdone in one step + +durative actionsticks until complete +SOC view +not god-mode +keep the lights on +Blue trains on logs - Red trains on recon - the rest is hidden diff --git a/docs/_static/figures/workflow.excalidraw b/docs/_static/figures/workflow.excalidraw new file mode 100644 index 0000000..c856b0f --- /dev/null +++ b/docs/_static/figures/workflow.excalidraw @@ -0,0 +1,797 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "t0012", + "type": "text", + "x": 28, + "y": 12, + "width": 280, + "height": 32, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 61, + "version": 1, + "versionNonce": 64, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "one tick", + "fontSize": 28, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "one tick", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0013", + "type": "rectangle", + "x": 24, + "y": 78, + "width": 155, + "height": 100, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FFD166", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 70, + "version": 1, + "versionNonce": 77, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0014", + "type": "text", + "x": 24, + "y": 100, + "width": 155, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 80, + "version": 1, + "versionNonce": 83, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "reset", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "reset", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0015", + "type": "text", + "x": 24, + "y": 132, + "width": 155, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 90, + "version": 1, + "versionNonce": 93, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "seeded net", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "seeded net", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "r0016", + "type": "rectangle", + "x": 210, + "y": 78, + "width": 165, + "height": 100, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#2EC4B6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 71, + "version": 1, + "versionNonce": 78, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0017", + "type": "text", + "x": 210, + "y": 100, + "width": 165, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 81, + "version": 1, + "versionNonce": 84, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "observe", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "observe", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0018", + "type": "text", + "x": 210, + "y": 132, + "width": 165, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 91, + "version": 1, + "versionNonce": 94, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SIEM + mask", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "SIEM + mask", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0019", + "type": "arrow", + "x": 179, + "y": 128, + "width": 31, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 101, + "version": 1, + "versionNonce": 110, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 31, + 0 + ] + ] + }, + { + "id": "r0020", + "type": "rectangle", + "x": 406, + "y": 78, + "width": 185, + "height": 100, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#7B6CF6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 72, + "version": 1, + "versionNonce": 79, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0021", + "type": "text", + "x": 406, + "y": 100, + "width": 185, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 82, + "version": 1, + "versionNonce": 85, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "act", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "act", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0022", + "type": "text", + "x": 406, + "y": 132, + "width": 185, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 92, + "version": 1, + "versionNonce": 95, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Red / Blue", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Red / Blue", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0023", + "type": "arrow", + "x": 375, + "y": 128, + "width": 31, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 102, + "version": 1, + "versionNonce": 111, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 31, + 0 + ] + ] + }, + { + "id": "r0024", + "type": "rectangle", + "x": 622, + "y": 78, + "width": 155, + "height": 100, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#FF9F43", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 73, + "version": 1, + "versionNonce": 80, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0025", + "type": "text", + "x": 622, + "y": 100, + "width": 155, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 83, + "version": 1, + "versionNonce": 86, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "resolve", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "resolve", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0026", + "type": "text", + "x": 622, + "y": 132, + "width": 155, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 93, + "version": 1, + "versionNonce": 96, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "conflicts", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "conflicts", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0027", + "type": "arrow", + "x": 591, + "y": 128, + "width": 31, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 103, + "version": 1, + "versionNonce": 112, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 31, + 0 + ] + ] + }, + { + "id": "r0028", + "type": "rectangle", + "x": 808, + "y": 78, + "width": 148, + "height": 100, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "#6BCB77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 74, + "version": 1, + "versionNonce": 81, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0029", + "type": "text", + "x": 808, + "y": 100, + "width": 148, + "height": 28, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 84, + "version": 1, + "versionNonce": 87, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "log", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "log", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t0030", + "type": "text", + "x": 808, + "y": 132, + "width": 148, + "height": 24, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 94, + "version": 1, + "versionNonce": 97, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Sysmon-like", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Sysmon-like", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a0031", + "type": "arrow", + "x": 777, + "y": 128, + "width": 31, + "height": 0, + "angle": 0, + "strokeColor": "#1B4B6B", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 104, + "version": 1, + "versionNonce": 113, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 31, + 0 + ] + ] + }, + { + "id": "r0032", + "type": "rectangle", + "x": 220, + "y": 200, + "width": 527, + "height": 36, + "angle": -0.026, + "strokeColor": "#2B2118", + "backgroundColor": "#8EC5FF", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 110, + "version": 1, + "versionNonce": 117, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t0033", + "type": "text", + "x": 220, + "y": 202, + "width": 527, + "height": 32, + "angle": 0, + "strokeColor": "#2B2118", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 111, + "version": 1, + "versionNonce": 114, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "exploits and isolates take time - logs can be late", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "exploits and isolates take time - logs can be late", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#FFF6DC", + "currentItemFontFamily": 2, + "width": 980, + "height": 270 + }, + "files": {} +} \ No newline at end of file diff --git a/docs/_static/figures/workflow.svg b/docs/_static/figures/workflow.svg new file mode 100644 index 0000000..78c4d3f --- /dev/null +++ b/docs/_static/figures/workflow.svg @@ -0,0 +1,18 @@ + +one tick + + + + +one tick +resetseeded net +observeSIEM + mask +actRed / Blue +resolveconflicts +logSysmon-like + + + + +exploits and isolates take time - logs can be late + diff --git a/docs/_templates/page.html b/docs/_templates/page.html new file mode 100644 index 0000000..3907aa5 --- /dev/null +++ b/docs/_templates/page.html @@ -0,0 +1,17 @@ +{% extends "!page.html" %} + +{% block content %} + +{{ super() }} +{% endblock %} From ab6f7cf0eea73861f9d4f2cd8158830b548285de Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:15:48 +0200 Subject: [PATCH 5/8] Rewrite the README as a gym first-run, matching the docs kit. Keep the arXiv citation and CybORG/DSTG license notices. Co-authored-by: Cursor --- README.md | 192 +++++++++++++++++++++++++----------------------------- 1 file changed, 90 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 5200f16..25421a7 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,125 @@
- Version 3.1.0 - Python 3.12+ - JAX - PettingZoo - MITRE ATT&CK +NetForge + +# NetForge + +**Cybersecurity gym for reinforcement learning** + +Red attacks a network. Blue contains it from SIEM, not from an oracle. +PettingZoo (1 Red + 3 Blue) or Gymnasium (one Blue vs scripted Red). + +**Python 3.12+** · **PettingZoo** · **Gymnasium** · **Not on PyPI** + +[Docs](https://reforcemind.github.io/NetForge_RL/) · +[Start](https://reforcemind.github.io/NetForge_RL/getting-started.html) · +[Python](https://reforcemind.github.io/NetForge_RL/python-quickstart.html) · +[Cyber](https://reforcemind.github.io/NetForge_RL/cybersec/overview.html) · +[Contributing](CONTRIBUTING.md) + +

+ Red, Blue, and the network +

-

NetForge RL

- -

Multi-agent cybersecurity environment for RL research. Red team vs Blue team on generated networks.

- ---- - -## Why NetForge - -- **Standardized API** — PettingZoo `ParallelEnv`, Gymnasium spaces, conformance-tested. - A `gymnasium.Env` single-agent facade (`NetForgeSingleAgentEnv`) drops straight into - Stable-Baselines3 or CleanRL. -- **Partial observability that means something** — Blue reads a filtered, optionally - *delayed* SIEM feed; Red must recon a host before it can exploit it. -- **Real telemetry** — actions emit real Windows/Sysmon event XML, encoded into - observations by an NLP pipeline. Point an OpenAI, Anthropic, Google, or vLLM client at - the raw logs and let it play SOC analyst. -- **MITRE ATT&CK aligned** — actions carry ATT&CK technique IDs, and CVE identifiers are - used as abstract vulnerability labels (no real exploit code); every episode reports which - ATT&CK techniques Red actually exercised. -- **Reproducible to the bit** — a fixed seed replays observations, SIEM embeddings, infos, - and rewards identically. Test-guaranteed, not just claimed. -- **Tunable difficulty** — named `easy` / `medium` / `hard` presets plus a frozen held-out - evaluation split, so results are comparable across runs. -- **Diagnostic probes + capability cards** — 6 targeted probes (memory, attention, - temporal, precision, safety, generalization) score *what* a policy can and can't do, - summarized as a per-policy radar chart. -- **Deception as a mechanic** — decoys and honeytokens with a `deception_efficacy` metric - quantifying how much of Red's effort was wasted on traps. -- **Optional graph observations** — an experimental wrapper exposes hosts as nodes and - reachability as edges (fog-of-war aware); the benchmarked default is the fixed-shape array. -- **Self-play & Elo** — a population tournament rates every red and blue policy on one - shared Elo ladder. -- **Fast** — the JAX backend reaches **~2.5×10⁵ env-steps/s (~1.0M agent-steps/s)** at batch - 4096 on CPU. It runs a reduced transition core with an in-kernel scalar alert (not the full - SIEM text pipeline), so it is a throughput surrogate, not a replica of the Python engine. -- **Trained, not just scripted, baselines** — a JIT-fused IPPO trainer whose entire - rollout runs on-device; a committed run learns mean reward 0.06 → 0.71 on `ransomware`. - -## Install +## 📰 News -```bash -pip install 'netforge_rl[jax] @ git+https://github.com/reforcemind/NetForge_RL' -``` +**🗓️ September 2026** + +- 🛡️ **20** — Gym ids: PettingZoo `netforge/ransomware-v4` (1 Red + 3 Blue) and Gymnasium `NetForge/Blue-v4`. Blue trains on SIEM belief graphs, not the true map. +- 📟 **20** — `netforge` CLI: `run`, `evaluate`, `questions`, HTML replay. Packs for hospital, cloud, APT, IoT, OT. +- 🏗️ **20** — Arena train / dev / hidden splits. Typed `EnvConfig`. Sphinx docs, same kit as [FlowEdge](https://github.com/reforcemind/FlowEdge). + +🏷️ [Changelog](docs/changelog.md) · 📘 [Docs](https://reforcemind.github.io/NetForge_RL/) + +*** + +## The problem + +A defender does not get the true compromise map. The SOC sees Sysmon-like logs +that can be late, dropped, or noisy. Red starts blind and only sees hosts it has +discovered. Exploits and isolates take ticks, not one step. + +A gym that hands Blue an oracle, pays for isolating the whole net, or scores +mean reward against one scripted Red is training the wrong job. -Extras: `jax` (vectorized backend), `render` (visualization), `finetune` (LLM PEFT), -`rllib` (Ray multi-agent training). See the -[install guide](https://reforcemind.github.io/NetForge_RL/quickstart/) for the full list. +## What NetForge does -## Quick start +Each tick, agents pick `[action_type, host_index]`. Actions run, conflicts +resolve, SIEM fires, Blue obs update from the buffer. + +**Red** — discover, exploit CVEs, escalate, exfil, or hit a PLC. +**Blue** — isolate, ACL, restore, decoys (`169.254.x.x`). Quiet Red may never +show up in logs. +**Kinetic** — `OverloadPLC` can end the episode for Blue. ```python -from netforge_rl.environment import make_env +import netforge_rl +import gymnasium as gym +from pettingzoo import make -env = make_env('medium', scenario_type='ransomware', seed=0) +env = make("parallel", "netforge/ransomware-v4", max_ticks=80) obs, infos = env.reset(seed=0) -while env.agents: - actions = {a: env.action_space(a).sample() for a in env.agents} - obs, rewards, term, trunc, infos = env.step(actions) - if all(term.values()) or all(trunc.values()): - break +blue = gym.make("NetForge/Blue-v4", max_ticks=80) ``` -### JAX vectorized (4096 environments) +

+ reset, observe SIEM, act, next tick +

-```python -import jax -from netforge_rl.backends.jax import VectorEnvSpec -from netforge_rl.bridges.jaxmarl import JaxMARLEnv, random_action_dict - -env = JaxMARLEnv(spec=VectorEnvSpec(n_hosts=100, n_red=1, n_blue=3), batch_size=4096) -key = jax.random.PRNGKey(0) -obs, state = env.reset(key) -obs, state, reward, done, info = env.step(key, state, random_action_dict(env, key)) -``` +## Why this, not the usual stack -### LLM as a Blue agent +

+ SOC view vs god-mode, delayed logs, SLA vs isolate-all +

-```python -from netforge_rl.semantic import state_to_text, parse_action +Blue never sees the true map. Logs can lag. Isolate-everything wrecks SLA. +Eval uses a Red population, not one campaign. Trainers (MAPPO / QMIX / …) stay +in your repo. -prompt = state_to_text(env.to_envstate(), agent_id='blue_dmz') -# send prompt to your model, then: -action_idx = parse_action(model_reply, 'blue_dmz', sorted(env.global_state.all_hosts)) -``` +## First run + +Not on PyPI. Python 3.12+. -See the [Quick Start Guide](https://reforcemind.github.io/NetForge_RL/quickstart/) for -difficulty presets, baselines, diagnostics, and the Gymnasium single-agent wrapper. +```bash +python -m pip install 'netforge-rl @ git+https://github.com/reforcemind/NetForge_RL' +netforge run hospital_ransomware --replay replay.html +``` -## The five scenarios +From a clone: -| Scenario | Red objective | Blue objective | Terminal condition | -|---|---|---|---| -| `ransomware` | Compromise Corporate + Secure | Contain, restore, avoid downtime | all Corporate/Secure compromised, or PLC kinetic | -| `apt_espionage` | Stealthy persistence + exfiltration | Detect and isolate every foothold | every infected host isolated | -| `cloud_hybrid` | Breach the Secure enclave | Protect the Secure subnet SLA | every Secure host compromised | -| `iot_grid` | Take the grid controllers | Keep controllers healthy | all controllers compromised | -| `ot_stuxnet` | Drive a PLC to kinetic destruction | Prevent physical damage | any PLC kinetic destruction | +```bash +git clone https://github.com/reforcemind/NetForge_RL.git +cd NetForge_RL +pip install -e ".[dev]" +netforge run ransomware --seed 0 --max-ticks 40 +``` -## Package layout +```bash +python -m netforge --help +``` -| Package | Purpose | +| Piece | | |---|---| -| `environment/` | PettingZoo `NetForgeRLEnv`, difficulty presets, curriculum, Gymnasium facade | -| `scenarios/` | 5 reward/objective families sharing one `BaseScenario` | -| `actions/red`, `actions/blue` | ATT&CK-aligned capabilities + technique mapping | -| `siem/`, `nlp/` | Sysmon/Windows log synthesis, NLP encoding, OCSF-style export | -| `backends/jax/` | Vectorized `vmap`/`jit` transition kernel + NumPy reference | -| `baselines/` | Random, heuristic, kill-chain red, JAX IPPO | -| `diagnostics/` | Capability probes, oracle information-asymmetry, capability cards | -| `bridges/` | RLlib, JaxMARL, CleanRL, DLPack adapters | -| `semantic/` | LLM SOC agents, prompt grammars, fine-tuning recipes | +| Agents | `red_operator`, `blue_dmz`, `blue_internal`, `blue_restricted` | +| Action | `MultiDiscrete([32, 100])`, mask `int8[132]` | +| Blue obs | vector + SIEM embedding + belief graph | +| Families | ransomware, APT, cloud, IoT, OT/Stuxnet | +| Trainers | your code. Not bundled. | + +Gate: `scripts/verify_all.sh` or `scripts/verify_local.ps1`. GitHub Actions is ruff + pytest + Sphinx. `docs.yml` deploys the site to GitHub Pages on `main`. ## Citation ```bibtex @misc{jankowski2026netforgerlmultiagentsimulation, - title={NetForge RL: A Multi-Agent Simulation Environment for Cyber Defense with Durative Actions}, + title={NetForge RL: A Multi-Agent Simulation Environment for Cyber Defense with Durative Actions}, author={Igor Jankowski}, year={2026}, eprint={2604.09523}, archivePrefix={arXiv}, primaryClass={cs.LG}, - url={https://arxiv.org/abs/2604.09523}, + url={https://arxiv.org/abs/2604.09523}, } ``` -## License - -MIT — see [LICENSE](LICENSE). +MIT, with CybORG / DSTG notices in [LICENSE](LICENSE). From e3e83670a06f2a16fd3bdd20eb159cc4f7523259 Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:21:48 +0200 Subject: [PATCH 6/8] Make ruff format happy on the example submission and render test. Unindent the evaluate command in the docstring and construct FrameRecorder before asserting empty save. Co-authored-by: Cursor --- examples/submissions/heuristic_blue.py | 2 +- tests/render/test_render_pipeline.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/submissions/heuristic_blue.py b/examples/submissions/heuristic_blue.py index a8ac9a9..8584b1a 100644 --- a/examples/submissions/heuristic_blue.py +++ b/examples/submissions/heuristic_blue.py @@ -1,6 +1,6 @@ """CAGE-style ``make_blue`` example. - netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 --max-ticks 20 +netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 --max-ticks 20 """ NAME = 'heuristic-blue' diff --git a/tests/render/test_render_pipeline.py b/tests/render/test_render_pipeline.py index 75bf41c..daf722d 100644 --- a/tests/render/test_render_pipeline.py +++ b/tests/render/test_render_pipeline.py @@ -68,6 +68,8 @@ def test_frame_recorder_append_and_len() -> None: def test_frame_recorder_save_requires_frames(tmp_path) -> None: pytest.importorskip('moviepy') from netforge_rl.render.recorder import FrameRecorder + + rec = FrameRecorder(fps=4) with pytest.raises(ValueError): rec.save(tmp_path / 'empty.gif') From c521b821ba6d698a3fdfefca6fbb6f95269a4fb6 Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:25:00 +0200 Subject: [PATCH 7/8] Pin ruff lint to pycodestyle and pyflakes so CI matches the old default. Ruff 0.16 treats a bare ignore list as a much larger rule set, which fails on the existing simulator files. Co-authored-by: Cursor --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 09d5284..a22807c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,7 @@ docstring-code-line-length = "dynamic" "netforge_rl/semantic/clients/*.py" = ["F401"] "netforge_rl/render/replay_html.py" = ["E501"] [tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] ignore = ["E701", "E402", "E702"] [tool.setuptools.packages.find] From 5b896f41212246769a063980cbcb4c1a3ed1a4c6 Mon Sep 17 00:00:00 2001 From: elprofesoriqo Date: Sun, 20 Sep 2026 15:25:04 +0200 Subject: [PATCH 8/8] Format Python snippets in docs so ruff format --check passes. Quote style is single, including fenced examples and the figure builder. Co-authored-by: Cursor --- README.md | 4 +- docs/_static/figures/build.py | 839 +++++++++++++++++++++++----------- docs/getting-started.md | 2 +- docs/index.md | 4 +- docs/interop/pettingzoo.md | 4 +- docs/interop/soc_export.md | 4 +- docs/python-quickstart.md | 4 +- docs/submissions.md | 4 +- docs/training/rllib.md | 12 +- docs/training/scenarios.md | 4 +- 10 files changed, 584 insertions(+), 297 deletions(-) diff --git a/README.md b/README.md index 25421a7..e8824c1 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ import netforge_rl import gymnasium as gym from pettingzoo import make -env = make("parallel", "netforge/ransomware-v4", max_ticks=80) +env = make('parallel', 'netforge/ransomware-v4', max_ticks=80) obs, infos = env.reset(seed=0) -blue = gym.make("NetForge/Blue-v4", max_ticks=80) +blue = gym.make('NetForge/Blue-v4', max_ticks=80) ```

diff --git a/docs/_static/figures/build.py b/docs/_static/figures/build.py index 3d98970..6163fa5 100644 --- a/docs/_static/figures/build.py +++ b/docs/_static/figures/build.py @@ -7,25 +7,25 @@ OUT = Path(__file__).resolve().parent -INK = "#2B2118" -TITLE = "#1B4B6B" -CANVAS = "#FFF6DC" -EDGE = "#E8D7A8" -SHADOW = "#E8C98A" -YEL = "#FFD166" -TEAL = "#2EC4B6" -PINK = "#FF8FAB" -ORANGE = "#FF9F43" -PURPLE = "#7B6CF6" -GREEN = "#6BCB77" -CREAM = "#FFF1C2" -BLUE = "#8EC5FF" -RED = "#FF6B6B" -FONT = "Trebuchet MS, Segoe UI, sans-serif" +INK = '#2B2118' +TITLE = '#1B4B6B' +CANVAS = '#FFF6DC' +EDGE = '#E8D7A8' +SHADOW = '#E8C98A' +YEL = '#FFD166' +TEAL = '#2EC4B6' +PINK = '#FF8FAB' +ORANGE = '#FF9F43' +PURPLE = '#7B6CF6' +GREEN = '#6BCB77' +CREAM = '#FFF1C2' +BLUE = '#8EC5FF' +RED = '#FF6B6B' +FONT = 'Trebuchet MS, Segoe UI, sans-serif' def header(title: str, w: int, h: int, mid: str | None = None) -> str: - extra = "" + extra = '' if mid: extra = ( f' str: def _slug(title: str) -> str: - return "".join(ch if ch.isalnum() else "-" for ch in title.lower()).strip("-")[:24] + return ''.join(ch if ch.isalnum() else '-' for ch in title.lower()).strip('-')[:24] def close() -> str: - return "\n" + return '\n' -def card(x: float, y: float, w: float, h: float, fill: str, rot: float, inner: str) -> str: +def card( + x: float, y: float, w: float, h: float, fill: str, rot: float, inner: str +) -> str: cx = x + w / 2 cy = y + h / 2 return ( f'' f'' f'' - f"{inner}\n" + f'{inner}\n' ) def xml_escape(text: str) -> str: - return text.replace("&", "&").replace("<", "<").replace(">", ">") - - -def t(x: float, y: float, size: int, weight: str, fill: str, text: str, anchor: str = "middle") -> str: + return text.replace('&', '&').replace('<', '<').replace('>', '>') + + +def t( + x: float, + y: float, + size: int, + weight: str, + fill: str, + text: str, + anchor: str = 'middle', +) -> str: return ( f'{xml_escape(text)}' ) -def chip(x: float, y: float, w: float, h: float, fill: str, rot: float, label: str) -> str: +def chip( + x: float, y: float, w: float, h: float, fill: str, rot: float, label: str +) -> str: cx = x + w / 2 cy = y + h / 2 return ( @@ -81,7 +93,9 @@ def chip(x: float, y: float, w: float, h: float, fill: str, rot: float, label: s ) -def arrow(x1: float, y1: float, x2: float, y2: float, title: str, qy: float | None = None) -> str: +def arrow( + x1: float, y1: float, x2: float, y2: float, title: str, qy: float | None = None +) -> str: mid = (x1 + x2) / 2 q = y1 if qy is None else qy return ( @@ -92,8 +106,8 @@ def arrow(x1: float, y1: float, x2: float, y2: float, title: str, qy: float | No def write(name: str, body: str) -> None: path = OUT / name - path.write_text(body, encoding="utf-8") - print("wrote", path.name, path.stat().st_size) + path.write_text(body, encoding='utf-8') + print('wrote', path.name, path.stat().st_size) # --------------------------------------------------------------------------- @@ -103,144 +117,145 @@ def write(name: str, body: str) -> None: _EID = 0 -def eid(prefix: str = "e") -> str: +def eid(prefix: str = 'e') -> str: global _EID _EID += 1 - return f"{prefix}{_EID:04d}" + return f'{prefix}{_EID:04d}' def e_rect(x, y, w, h, bg, angle=0.0, seed=1): return { - "id": eid("r"), - "type": "rectangle", - "x": x, - "y": y, - "width": w, - "height": h, - "angle": angle, - "strokeColor": INK, - "backgroundColor": bg, - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": None, - "roundness": {"type": 3}, - "seed": seed, - "version": 1, - "versionNonce": seed + 7, - "isDeleted": False, - "boundElements": None, - "updated": 1, - "link": None, - "locked": False, + 'id': eid('r'), + 'type': 'rectangle', + 'x': x, + 'y': y, + 'width': w, + 'height': h, + 'angle': angle, + 'strokeColor': INK, + 'backgroundColor': bg, + 'fillStyle': 'solid', + 'strokeWidth': 2, + 'strokeStyle': 'solid', + 'roughness': 1, + 'opacity': 100, + 'groupIds': [], + 'frameId': None, + 'roundness': {'type': 3}, + 'seed': seed, + 'version': 1, + 'versionNonce': seed + 7, + 'isDeleted': False, + 'boundElements': None, + 'updated': 1, + 'link': None, + 'locked': False, } -def e_text(x, y, w, h, text, color=INK, size=20, align="center", seed=2): +def e_text(x, y, w, h, text, color=INK, size=20, align='center', seed=2): return { - "id": eid("t"), - "type": "text", - "x": x, - "y": y, - "width": w, - "height": h, - "angle": 0, - "strokeColor": color, - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": None, - "roundness": None, - "seed": seed, - "version": 1, - "versionNonce": seed + 3, - "isDeleted": False, - "boundElements": None, - "updated": 1, - "link": None, - "locked": False, - "text": text, - "fontSize": size, - "fontFamily": 2, - "textAlign": align, - "verticalAlign": "middle", - "containerId": None, - "originalText": text, - "autoResize": True, - "lineHeight": 1.25, + 'id': eid('t'), + 'type': 'text', + 'x': x, + 'y': y, + 'width': w, + 'height': h, + 'angle': 0, + 'strokeColor': color, + 'backgroundColor': 'transparent', + 'fillStyle': 'solid', + 'strokeWidth': 1, + 'strokeStyle': 'solid', + 'roughness': 0, + 'opacity': 100, + 'groupIds': [], + 'frameId': None, + 'roundness': None, + 'seed': seed, + 'version': 1, + 'versionNonce': seed + 3, + 'isDeleted': False, + 'boundElements': None, + 'updated': 1, + 'link': None, + 'locked': False, + 'text': text, + 'fontSize': size, + 'fontFamily': 2, + 'textAlign': align, + 'verticalAlign': 'middle', + 'containerId': None, + 'originalText': text, + 'autoResize': True, + 'lineHeight': 1.25, } def e_arrow(x, y, w, h, seed=3): return { - "id": eid("a"), - "type": "arrow", - "x": x, - "y": y, - "width": w, - "height": h, - "angle": 0, - "strokeColor": TITLE, - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": None, - "roundness": {"type": 2}, - "seed": seed, - "version": 1, - "versionNonce": seed + 9, - "isDeleted": False, - "boundElements": None, - "updated": 1, - "link": None, - "locked": False, - "startBinding": None, - "endBinding": None, - "lastCommittedPoint": None, - "startArrowhead": None, - "endArrowhead": "arrow", - "points": [[0, 0], [w, h]], + 'id': eid('a'), + 'type': 'arrow', + 'x': x, + 'y': y, + 'width': w, + 'height': h, + 'angle': 0, + 'strokeColor': TITLE, + 'backgroundColor': 'transparent', + 'fillStyle': 'solid', + 'strokeWidth': 2, + 'strokeStyle': 'solid', + 'roughness': 1, + 'opacity': 100, + 'groupIds': [], + 'frameId': None, + 'roundness': {'type': 2}, + 'seed': seed, + 'version': 1, + 'versionNonce': seed + 9, + 'isDeleted': False, + 'boundElements': None, + 'updated': 1, + 'link': None, + 'locked': False, + 'startBinding': None, + 'endBinding': None, + 'lastCommittedPoint': None, + 'startArrowhead': None, + 'endArrowhead': 'arrow', + 'points': [[0, 0], [w, h]], } def scene(elements: list, w: int = 1180, h: int = 400) -> dict: return { - "type": "excalidraw", - "version": 2, - "source": "https://excalidraw.com", - "elements": elements, - "appState": { - "gridSize": None, - "viewBackgroundColor": CANVAS, - "currentItemFontFamily": 2, - "width": w, - "height": h, + 'type': 'excalidraw', + 'version': 2, + 'source': 'https://excalidraw.com', + 'elements': elements, + 'appState': { + 'gridSize': None, + 'viewBackgroundColor': CANVAS, + 'currentItemFontFamily': 2, + 'width': w, + 'height': h, }, - "files": {}, + 'files': {}, } def write_ex(name: str, elements: list, w: int = 1180, h: int = 400) -> None: path = OUT / name - path.write_text(json.dumps(scene(elements, w, h), indent=2), encoding="utf-8") - print("wrote", path.name, path.stat().st_size) + path.write_text(json.dumps(scene(elements, w, h), indent=2), encoding='utf-8') + print('wrote', path.name, path.stat().st_size) # --------------------------------------------------------------------------- # Diagrams # --------------------------------------------------------------------------- + def mark_svg() -> str: return f''' NetForge @@ -256,58 +271,117 @@ def mark_svg() -> str: def purpose(): - title = "the match" - parts = [header(title, 1180, 400, "Red attacks - Blue defends - SIEM in the middle")] + title = 'the match' + parts = [ + header(title, 1180, 400, 'Red attacks - Blue defends - SIEM in the middle') + ] parts.append( card( - 36, 78, 350, 220, RED, -0.08, - t(211, 156, 24, "800", TITLE, "Red") - + t(211, 188, 16, "600", INK, "discover, exploit") - + t(211, 208, 16, "600", INK, "escalate, exfil") - + t(211, 228, 16, "600", INK, "fog of war") - + t(211, 266, 14, "600", TITLE, "starts blind"), + 36, + 78, + 350, + 220, + RED, + -0.08, + t(211, 156, 24, '800', TITLE, 'Red') + + t(211, 188, 16, '600', INK, 'discover, exploit') + + t(211, 208, 16, '600', INK, 'escalate, exfil') + + t(211, 228, 16, '600', INK, 'fog of war') + + t(211, 266, 14, '600', TITLE, 'starts blind'), ) ) parts.append( card( - 414, 78, 350, 220, TEAL, 0.24, - t(589, 156, 24, "800", TITLE, "Network") - + t(589, 188, 16, "600", INK, "hosts, subnets") - + t(589, 208, 16, "600", INK, "firewalls, decoys") - + t(589, 228, 16, "600", INK, "durative actions") - + t(589, 266, 14, "600", TITLE, "the gym world"), + 414, + 78, + 350, + 220, + TEAL, + 0.24, + t(589, 156, 24, '800', TITLE, 'Network') + + t(589, 188, 16, '600', INK, 'hosts, subnets') + + t(589, 208, 16, '600', INK, 'firewalls, decoys') + + t(589, 228, 16, '600', INK, 'durative actions') + + t(589, 266, 14, '600', TITLE, 'the gym world'), ) ) parts.append( card( - 792, 78, 350, 220, BLUE, 0.10, - t(967, 156, 24, "800", TITLE, "Blue") - + t(967, 188, 16, "600", INK, "isolate, restore") - + t(967, 208, 16, "600", INK, "decoys, EDR") - + t(967, 228, 16, "600", INK, "reads SIEM only") - + t(967, 266, 14, "600", TITLE, "no oracle"), + 792, + 78, + 350, + 220, + BLUE, + 0.10, + t(967, 156, 24, '800', TITLE, 'Blue') + + t(967, 188, 16, '600', INK, 'isolate, restore') + + t(967, 208, 16, '600', INK, 'decoys, EDR') + + t(967, 228, 16, '600', INK, 'reads SIEM only') + + t(967, 266, 14, '600', TITLE, 'no oracle'), ) ) - parts.append(chip(70, 312, 148, 36, YEL, 0.9, "ransomware")) - parts.append(chip(470, 312, 174, 36, ORANGE, -1.4, "APT / cloud / OT")) - parts.append(chip(848, 312, 174, 36, PINK, -1.4, "1 Red + 3 Blue")) - parts.append(t(590, 378, 15, "800", TITLE, "PettingZoo parallel -> or Gymnasium Blue-v4")) + parts.append(chip(70, 312, 148, 36, YEL, 0.9, 'ransomware')) + parts.append(chip(470, 312, 174, 36, ORANGE, -1.4, 'APT / cloud / OT')) + parts.append(chip(848, 312, 174, 36, PINK, -1.4, '1 Red + 3 Blue')) + parts.append( + t(590, 378, 15, '800', TITLE, 'PettingZoo parallel -> or Gymnasium Blue-v4') + ) parts.append(close()) - write("purpose.svg", "".join(parts)) + write('purpose.svg', ''.join(parts)) write_ex( - "purpose.excalidraw", + 'purpose.excalidraw', [ - e_text(28, 12, 280, 32, "the match", TITLE, 28, "left", 11), - e_text(240, 44, 700, 24, "Red attacks - Blue defends - SIEM in the middle", TITLE, 18, "center", 12), + e_text(28, 12, 280, 32, 'the match', TITLE, 28, 'left', 11), + e_text( + 240, + 44, + 700, + 24, + 'Red attacks - Blue defends - SIEM in the middle', + TITLE, + 18, + 'center', + 12, + ), e_rect(36, 78, 350, 220, RED, -0.01, 21), - e_text(36, 150, 350, 40, "Red", TITLE, 28, "center", 22), - e_text(36, 190, 350, 80, "discover, exploit\nescalate, exfil\nstarts blind", INK, 18, "center", 23), + e_text(36, 150, 350, 40, 'Red', TITLE, 28, 'center', 22), + e_text( + 36, + 190, + 350, + 80, + 'discover, exploit\nescalate, exfil\nstarts blind', + INK, + 18, + 'center', + 23, + ), e_rect(414, 78, 350, 220, TEAL, 0.004, 31), - e_text(414, 150, 350, 40, "Network", TITLE, 28, "center", 32), - e_text(414, 190, 350, 80, "hosts, subnets\nfirewalls, decoys\ndurative actions", INK, 18, "center", 33), + e_text(414, 150, 350, 40, 'Network', TITLE, 28, 'center', 32), + e_text( + 414, + 190, + 350, + 80, + 'hosts, subnets\nfirewalls, decoys\ndurative actions', + INK, + 18, + 'center', + 33, + ), e_rect(792, 78, 350, 220, BLUE, 0.002, 41), - e_text(792, 150, 350, 40, "Blue", TITLE, 28, "center", 42), - e_text(792, 190, 350, 80, "isolate, restore\nreads SIEM only\nno oracle", INK, 18, "center", 43), + e_text(792, 150, 350, 40, 'Blue', TITLE, 28, 'center', 42), + e_text( + 792, + 190, + 350, + 80, + 'isolate, restore\nreads SIEM only\nno oracle', + INK, + 18, + 'center', + 43, + ), ], 1180, 400, @@ -315,223 +389,432 @@ def purpose(): def workflow(): - title = "one tick" + title = 'one tick' parts = [header(title, 980, 270)] boxes = [ - (24, 78, 155, 100, YEL, -0.13, "reset", "seeded net"), - (210, 78, 165, 100, TEAL, -0.07, "observe", "SIEM + mask"), - (406, 78, 185, 100, PURPLE, -0.15, "act", "Red / Blue"), - (622, 78, 155, 100, ORANGE, -0.25, "resolve", "conflicts"), - (808, 78, 148, 100, GREEN, -0.17, "log", "Sysmon-like"), + (24, 78, 155, 100, YEL, -0.13, 'reset', 'seeded net'), + (210, 78, 165, 100, TEAL, -0.07, 'observe', 'SIEM + mask'), + (406, 78, 185, 100, PURPLE, -0.15, 'act', 'Red / Blue'), + (622, 78, 155, 100, ORANGE, -0.25, 'resolve', 'conflicts'), + (808, 78, 148, 100, GREEN, -0.17, 'log', 'Sysmon-like'), ] for x, y, w, h, fill, rot, a, b in boxes: cx = x + w / 2 parts.append( - card(x, y, w, h, fill, rot, t(cx, 118, 16, "800", INK, a) + t(cx, 140, 14, "600", INK, b)) + card( + x, + y, + w, + h, + fill, + rot, + t(cx, 118, 16, '800', INK, a) + t(cx, 140, 14, '600', INK, b), + ) ) parts.append(arrow(179, 128, 210, 128, title, 133)) parts.append(arrow(375, 128, 406, 128, title, 126)) parts.append(arrow(591, 128, 622, 128, title, 128)) parts.append(arrow(777, 128, 808, 128, title, 129)) - parts.append(chip(220, 200, 527, 36, BLUE, -1.5, "exploits and isolates take time - logs can be late")) + parts.append( + chip( + 220, + 200, + 527, + 36, + BLUE, + -1.5, + 'exploits and isolates take time - logs can be late', + ) + ) parts.append(close()) - write("workflow.svg", "".join(parts)) - els = [e_text(28, 12, 280, 32, "one tick", TITLE, 28, "left", 61)] + write('workflow.svg', ''.join(parts)) + els = [e_text(28, 12, 280, 32, 'one tick', TITLE, 28, 'left', 61)] xs = [24, 210, 406, 622, 808] fills = [YEL, TEAL, PURPLE, ORANGE, GREEN] - labels = [("reset", "seeded net"), ("observe", "SIEM + mask"), ("act", "Red / Blue"), ("resolve", "conflicts"), ("log", "Sysmon-like")] + labels = [ + ('reset', 'seeded net'), + ('observe', 'SIEM + mask'), + ('act', 'Red / Blue'), + ('resolve', 'conflicts'), + ('log', 'Sysmon-like'), + ] ws = [155, 165, 185, 155, 148] for i, (x, fill, (a, b), w) in enumerate(zip(xs, fills, labels, ws)): els += [ e_rect(x, 78, w, 100, fill, 0, 70 + i), - e_text(x, 100, w, 28, a, INK, 20, "center", 80 + i), - e_text(x, 132, w, 24, b, INK, 16, "center", 90 + i), + e_text(x, 100, w, 28, a, INK, 20, 'center', 80 + i), + e_text(x, 132, w, 24, b, INK, 16, 'center', 90 + i), ] if i: - els.append(e_arrow(xs[i - 1] + ws[i - 1], 128, x - (xs[i - 1] + ws[i - 1]), 0, 100 + i)) + els.append( + e_arrow( + xs[i - 1] + ws[i - 1], 128, x - (xs[i - 1] + ws[i - 1]), 0, 100 + i + ) + ) els += [ e_rect(220, 200, 527, 36, BLUE, -0.026, 110), - e_text(220, 202, 527, 32, "exploits and isolates take time - logs can be late", INK, 16, "center", 111), + e_text( + 220, + 202, + 527, + 32, + 'exploits and isolates take time - logs can be late', + INK, + 16, + 'center', + 111, + ), ] - write_ex("workflow.excalidraw", els, 980, 270) + write_ex('workflow.excalidraw', els, 980, 270) def why_this(): - title = "what Blue actually sees" + title = 'what Blue actually sees' rows = [ - (YEL, "true compromise map", "simulator state", TEAL, "SIEM belief graph", "alerts + inventory"), - (PINK, "instant telemetry", "every exploit is obvious", GREEN, "delayed / noisy logs", "log_latency, drop, decoys"), - (ORANGE, "one giant isolate", "kill the network to win", PURPLE, "SLA vs security", "keep hosts online"), - (RED, "scripted campaign only", "one attacker forever", TEAL, "Red population", "random / heuristic / kill-chain"), - (PINK, "discrete instant acts", "done in one step", GREEN, "durative actions", "ticks until complete"), + ( + YEL, + 'true compromise map', + 'simulator state', + TEAL, + 'SIEM belief graph', + 'alerts + inventory', + ), + ( + PINK, + 'instant telemetry', + 'every exploit is obvious', + GREEN, + 'delayed / noisy logs', + 'log_latency, drop, decoys', + ), + ( + ORANGE, + 'one giant isolate', + 'kill the network to win', + PURPLE, + 'SLA vs security', + 'keep hosts online', + ), + ( + RED, + 'scripted campaign only', + 'one attacker forever', + TEAL, + 'Red population', + 'random / heuristic / kill-chain', + ), + ( + PINK, + 'discrete instant acts', + 'done in one step', + GREEN, + 'durative actions', + 'ticks until complete', + ), ] parts = [header(title, 980, 570)] y = 72 for i, (lc, lt, ls, rc, rt, rs) in enumerate(rows): parts.append( card( - 36, y, 380, 70, lc, 0.04 if i % 2 == 0 else -0.12, - t(226, y + 25, 16, "800", INK, lt) + t(226, y + 47, 14, "600", INK, ls), + 36, + y, + 380, + 70, + lc, + 0.04 if i % 2 == 0 else -0.12, + t(226, y + 25, 16, '800', INK, lt) + t(226, y + 47, 14, '600', INK, ls), ) ) parts.append(arrow(428, y + 35, 534, y + 35, title, y + 32)) parts.append( card( - 544, y, 400, 70, rc, -0.08 if i % 2 == 0 else 0.06, - t(744, y + 25, 16, "800", INK, rt) + t(744, y + 47, 14, "600", INK, rs), + 544, + y, + 400, + 70, + rc, + -0.08 if i % 2 == 0 else 0.06, + t(744, y + 25, 16, '800', INK, rt) + t(744, y + 47, 14, '600', INK, rs), ) ) y += 78 - parts.append(chip(36, 472, 180, 36, CREAM, -1.4, "SOC view")) - parts.append(chip(248, 472, 200, 36, CREAM, -0.8, "not god-mode")) - parts.append(chip(532, 472, 226, 36, YEL, -1.2, "keep the lights on")) - parts.append(t(490, 540, 14, "800", TITLE, "Blue trains on logs - Red trains on recon - the rest is hidden")) + parts.append(chip(36, 472, 180, 36, CREAM, -1.4, 'SOC view')) + parts.append(chip(248, 472, 200, 36, CREAM, -0.8, 'not god-mode')) + parts.append(chip(532, 472, 226, 36, YEL, -1.2, 'keep the lights on')) + parts.append( + t( + 490, + 540, + 14, + '800', + TITLE, + 'Blue trains on logs - Red trains on recon - the rest is hidden', + ) + ) parts.append(close()) - write("why-this.svg", "".join(parts)) - els = [e_text(28, 12, 520, 32, "what Blue actually sees", TITLE, 26, "left", 200)] + write('why-this.svg', ''.join(parts)) + els = [e_text(28, 12, 520, 32, 'what Blue actually sees', TITLE, 26, 'left', 200)] y = 72 for i, (lc, lt, ls, rc, rt, rs) in enumerate(rows): els += [ e_rect(36, y, 380, 70, lc, 0, 210 + i), - e_text(36, y + 8, 380, 28, lt, INK, 18, "center", 220 + i), - e_text(36, y + 36, 380, 24, ls, INK, 14, "center", 230 + i), + e_text(36, y + 8, 380, 28, lt, INK, 18, 'center', 220 + i), + e_text(36, y + 36, 380, 24, ls, INK, 14, 'center', 230 + i), e_arrow(416, y + 35, 128, 0, 240 + i), e_rect(544, y, 400, 70, rc, 0, 250 + i), - e_text(544, y + 8, 400, 28, rt, INK, 18, "center", 260 + i), - e_text(544, y + 36, 400, 24, rs, INK, 14, "center", 270 + i), + e_text(544, y + 8, 400, 28, rt, INK, 18, 'center', 260 + i), + e_text(544, y + 36, 400, 24, rs, INK, 14, 'center', 270 + i), ] y += 78 - write_ex("why-this.excalidraw", els, 980, 570) + write_ex('why-this.excalidraw', els, 980, 570) def status(): - title = "in the gym" + title = 'in the gym' parts = [header(title, 980, 520)] - def pill(x: float, y: float, w: float, fill: str, mark: str, name: str, rot: float) -> None: - parts.append(card(x, y, w, 56, fill, rot, "")) + def pill( + x: float, y: float, w: float, fill: str, mark: str, name: str, rot: float + ) -> None: + parts.append(card(x, y, w, 56, fill, rot, '')) parts.append( f'' ) - parts.append(t(x + 18, y + 33, 13, "800", INK, mark)) - parts.append(t(x + w / 2 + 10, y + 32, 13, "800", TITLE, name)) - - parts.append(t(36, 68, 16, "800", TITLE, "APIs", "start")) - pill(36, 80, 220, GREEN, "+", "PettingZoo", 0.11) - pill(268, 80, 250, GREEN, "+", "Gymnasium Blue-v4", 0.15) - pill(530, 80, 200, GREEN, "+", "AEC wrapper", 0.08) - - parts.append(t(36, 160, 16, "800", TITLE, "World", "start")) - pill(36, 172, 200, GREEN, "+", "SIEM logs", 0.25) - pill(248, 172, 210, GREEN, "+", "belief graphs", 0.06) - pill(470, 172, 200, GREEN, "+", "decoys / OT", -0.05) - pill(682, 172, 190, TEAL, "+", "HTML replay", -0.20) - - parts.append(t(36, 252, 16, "800", TITLE, "Agents", "start")) - pill(36, 264, 200, GREEN, "+", "heuristics", 0.14) - pill(248, 264, 220, GREEN, "+", "kill-chain Red", 0.02) - pill(480, 264, 210, TEAL, "+", "scripted Blue", -0.11) - - parts.append(t(36, 344, 16, "800", TITLE, "Your code", "start")) - pill(36, 356, 160, CREAM, "x", "MAPPO", 0.11) - pill(208, 356, 150, CREAM, "x", "QMIX", -0.21) - pill(370, 356, 190, CREAM, "x", "GNN trainer", -0.18) - pill(572, 356, 200, CREAM, "x", "LLM agent", -0.08) + parts.append(t(x + 18, y + 33, 13, '800', INK, mark)) + parts.append(t(x + w / 2 + 10, y + 32, 13, '800', TITLE, name)) + + parts.append(t(36, 68, 16, '800', TITLE, 'APIs', 'start')) + pill(36, 80, 220, GREEN, '+', 'PettingZoo', 0.11) + pill(268, 80, 250, GREEN, '+', 'Gymnasium Blue-v4', 0.15) + pill(530, 80, 200, GREEN, '+', 'AEC wrapper', 0.08) + + parts.append(t(36, 160, 16, '800', TITLE, 'World', 'start')) + pill(36, 172, 200, GREEN, '+', 'SIEM logs', 0.25) + pill(248, 172, 210, GREEN, '+', 'belief graphs', 0.06) + pill(470, 172, 200, GREEN, '+', 'decoys / OT', -0.05) + pill(682, 172, 190, TEAL, '+', 'HTML replay', -0.20) + + parts.append(t(36, 252, 16, '800', TITLE, 'Agents', 'start')) + pill(36, 264, 200, GREEN, '+', 'heuristics', 0.14) + pill(248, 264, 220, GREEN, '+', 'kill-chain Red', 0.02) + pill(480, 264, 210, TEAL, '+', 'scripted Blue', -0.11) + + parts.append(t(36, 344, 16, '800', TITLE, 'Your code', 'start')) + pill(36, 356, 160, CREAM, 'x', 'MAPPO', 0.11) + pill(208, 356, 150, CREAM, 'x', 'QMIX', -0.21) + pill(370, 356, 190, CREAM, 'x', 'GNN trainer', -0.18) + pill(572, 356, 200, CREAM, 'x', 'LLM agent', -0.08) parts.append( - t(36, 448, 14, "800", TITLE, "entry: gym.make / pettingzoo.make - netforge run / evaluate", "start") + t( + 36, + 448, + 14, + '800', + TITLE, + 'entry: gym.make / pettingzoo.make - netforge run / evaluate', + 'start', + ) + ) + parts.append( + t( + 36, + 478, + 14, + '800', + TITLE, + '+ ships in this repo x train elsewhere, run here', + 'start', + ) ) - parts.append(t(36, 478, 14, "800", TITLE, "+ ships in this repo x train elsewhere, run here", "start")) parts.append(close()) - write("status.svg", "".join(parts)) - els = [e_text(28, 12, 280, 32, "in the gym", TITLE, 26, "left", 300)] + write('status.svg', ''.join(parts)) + els = [e_text(28, 12, 280, 32, 'in the gym', TITLE, 26, 'left', 300)] rows_e = [ - (80, "APIs", [(36, 220, "PettingZoo", GREEN), (268, 250, "Gymnasium Blue-v4", GREEN), (530, 200, "AEC wrapper", GREEN)]), - (172, "World", [(36, 200, "SIEM logs", GREEN), (248, 210, "belief graphs", GREEN), (470, 200, "decoys / OT", GREEN), (682, 190, "HTML replay", TEAL)]), - (264, "Agents", [(36, 200, "heuristics", GREEN), (248, 220, "kill-chain Red", GREEN), (480, 210, "scripted Blue", TEAL)]), - (356, "Your code", [(36, 160, "MAPPO", CREAM), (208, 150, "QMIX", CREAM), (370, 190, "GNN trainer", CREAM), (572, 200, "LLM agent", CREAM)]), + ( + 80, + 'APIs', + [ + (36, 220, 'PettingZoo', GREEN), + (268, 250, 'Gymnasium Blue-v4', GREEN), + (530, 200, 'AEC wrapper', GREEN), + ], + ), + ( + 172, + 'World', + [ + (36, 200, 'SIEM logs', GREEN), + (248, 210, 'belief graphs', GREEN), + (470, 200, 'decoys / OT', GREEN), + (682, 190, 'HTML replay', TEAL), + ], + ), + ( + 264, + 'Agents', + [ + (36, 200, 'heuristics', GREEN), + (248, 220, 'kill-chain Red', GREEN), + (480, 210, 'scripted Blue', TEAL), + ], + ), + ( + 356, + 'Your code', + [ + (36, 160, 'MAPPO', CREAM), + (208, 150, 'QMIX', CREAM), + (370, 190, 'GNN trainer', CREAM), + (572, 200, 'LLM agent', CREAM), + ], + ), ] for y, lab, items in rows_e: - els.append(e_text(36, y - 28, 200, 24, lab, TITLE, 18, "left", y)) + els.append(e_text(36, y - 28, 200, 24, lab, TITLE, 18, 'left', y)) for x, w, name, fill in items: - els += [e_rect(x, y, w, 56, fill, 0, y + x), e_text(x, y + 12, w, 32, name, TITLE, 16, "center", y + x + 1)] - write_ex("status.excalidraw", els, 980, 520) + els += [ + e_rect(x, y, w, 56, fill, 0, y + x), + e_text(x, y + 12, w, 32, name, TITLE, 16, 'center', y + x + 1), + ] + write_ex('status.excalidraw', els, 980, 520) def arena(): - title = "eval splits" - parts = [header(title, 1180, 340, "train / dev / hidden - same metrics")] + title = 'eval splits' + parts = [header(title, 1180, 340, 'train / dev / hidden - same metrics')] cols = [ - (36, YEL, "train", "public", "procedural packs", "learn here"), - (414, TEAL, "dev", "public", "held-out offset", "pick a checkpoint"), - (792, PINK, "hidden", "organizer", "reserved + OOD", "final check"), + (36, YEL, 'train', 'public', 'procedural packs', 'learn here'), + (414, TEAL, 'dev', 'public', 'held-out offset', 'pick a checkpoint'), + (792, PINK, 'hidden', 'organizer', 'reserved + OOD', 'final check'), ] for x, fill, name, who, how, use in cols: cx = x + 175 parts.append( card( - x, 78, 350, 180, fill, 0.12 if fill == YEL else (-0.1 if fill == TEAL else 0.06), - t(cx, 130, 28, "800", TITLE, name) - + t(cx, 168, 16, "600", INK, who) - + t(cx, 192, 16, "600", INK, how) - + t(cx, 228, 14, "600", TITLE, use), + x, + 78, + 350, + 180, + fill, + 0.12 if fill == YEL else (-0.1 if fill == TEAL else 0.06), + t(cx, 130, 28, '800', TITLE, name) + + t(cx, 168, 16, '600', INK, who) + + t(cx, 192, 16, '600', INK, how) + + t(cx, 228, 14, '600', TITLE, use), ) ) - parts.append(chip(200, 274, 780, 36, BLUE, -0.7, "mission, SLA, false positives, kinetic - not return alone")) + parts.append( + chip( + 200, + 274, + 780, + 36, + BLUE, + -0.7, + 'mission, SLA, false positives, kinetic - not return alone', + ) + ) parts.append(close()) - write("arena.svg", "".join(parts)) + write('arena.svg', ''.join(parts)) els = [ - e_text(28, 12, 280, 32, "eval splits", TITLE, 28, "left", 400), - e_text(280, 44, 620, 24, "train / dev / hidden - same metrics", TITLE, 18, "center", 401), + e_text(28, 12, 280, 32, 'eval splits', TITLE, 28, 'left', 400), + e_text( + 280, + 44, + 620, + 24, + 'train / dev / hidden - same metrics', + TITLE, + 18, + 'center', + 401, + ), ] for x, fill, name, who, how, use in cols: els += [ e_rect(x, 78, 350, 180, fill, 0, 410 + x), - e_text(x, 110, 350, 40, name, TITLE, 28, "center", 411 + x), - e_text(x, 160, 350, 70, f"{who}\n{how}\n{use}", INK, 18, "center", 412 + x), + e_text(x, 110, 350, 40, name, TITLE, 28, 'center', 411 + x), + e_text(x, 160, 350, 70, f'{who}\n{how}\n{use}', INK, 18, 'center', 412 + x), ] - write_ex("arena.excalidraw", els, 1180, 340) + write_ex('arena.excalidraw', els, 1180, 340) def belief(): - title = "SIEM vs full state" - parts = [header(title, 980, 300, "Blue trains on logs - full map is diagnostics")] + title = 'SIEM vs full state' + parts = [ + header(title, 980, 300, 'Blue trains on logs - full map is diagnostics') + ] parts.append( card( - 36, 78, 420, 160, YEL, -0.12, - t(246, 130, 22, "800", TITLE, "SIEM belief") - + t(246, 162, 16, "600", INK, "what a SOC would see") - + t(246, 186, 16, "600", INK, "partial, delayed, noisy") - + t(246, 214, 14, "600", TITLE, "default for Blue"), + 36, + 78, + 420, + 160, + YEL, + -0.12, + t(246, 130, 22, '800', TITLE, 'SIEM belief') + + t(246, 162, 16, '600', INK, 'what a SOC would see') + + t(246, 186, 16, '600', INK, 'partial, delayed, noisy') + + t(246, 214, 14, '600', TITLE, 'default for Blue'), ) ) parts.append(arrow(468, 158, 524, 158, title, 150)) parts.append( card( - 534, 78, 410, 160, PINK, 0.14, - t(739, 130, 22, "800", TITLE, "oracle graph") - + t(739, 162, 16, "600", INK, "true compromise") - + t(739, 186, 16, "600", INK, "debug / replay") - + t(739, 214, 14, "600", TITLE, "not a Blue obs"), + 534, + 78, + 410, + 160, + PINK, + 0.14, + t(739, 130, 22, '800', TITLE, 'oracle graph') + + t(739, 162, 16, '600', INK, 'true compromise') + + t(739, 186, 16, '600', INK, 'debug / replay') + + t(739, 214, 14, '600', TITLE, 'not a Blue obs'), ) ) - parts.append(chip(220, 250, 540, 36, TEAL, -1.1, "netforge questions belief-vs-oracle")) + parts.append( + chip(220, 250, 540, 36, TEAL, -1.1, 'netforge questions belief-vs-oracle') + ) parts.append(close()) - write("belief.svg", "".join(parts)) + write('belief.svg', ''.join(parts)) write_ex( - "belief.excalidraw", + 'belief.excalidraw', [ - e_text(28, 12, 400, 32, "SIEM vs full state", TITLE, 26, "left", 500), + e_text(28, 12, 400, 32, 'SIEM vs full state', TITLE, 26, 'left', 500), e_rect(36, 78, 420, 160, YEL, -0.002, 501), - e_text(36, 120, 420, 80, "SIEM belief\nwhat a SOC would see", INK, 20, "center", 502), + e_text( + 36, + 120, + 420, + 80, + 'SIEM belief\nwhat a SOC would see', + INK, + 20, + 'center', + 502, + ), e_arrow(456, 158, 78, 0, 503), e_rect(534, 78, 410, 160, PINK, 0.002, 504), - e_text(534, 120, 410, 80, "oracle graph\ntrue compromise, debug only", INK, 20, "center", 505), + e_text( + 534, + 120, + 410, + 80, + 'oracle graph\ntrue compromise, debug only', + INK, + 20, + 'center', + 505, + ), ], 980, 300, ) + def main() -> None: (OUT / 'mark.svg').write_text(mark_svg(), encoding='utf-8') print('wrote mark.svg') diff --git a/docs/getting-started.md b/docs/getting-started.md index d895fa8..117437e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -21,7 +21,7 @@ import netforge_rl from pettingzoo import make from netforge_rl.environment.graph_wrapper import GraphObservationWrapper -env = GraphObservationWrapper(make("parallel", "netforge/ransomware-v4", max_ticks=80)) +env = GraphObservationWrapper(make('parallel', 'netforge/ransomware-v4', max_ticks=80)) obs, infos = env.reset(seed=0) ``` diff --git a/docs/index.md b/docs/index.md index e316fea..dd69a2c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,9 +112,9 @@ import netforge_rl import gymnasium as gym from pettingzoo import make -env = make("parallel", "netforge/ransomware-v4", max_ticks=80) +env = make('parallel', 'netforge/ransomware-v4', max_ticks=80) obs, infos = env.reset(seed=0) -blue = gym.make("NetForge/Blue-v4", max_ticks=80) +blue = gym.make('NetForge/Blue-v4', max_ticks=80) ``` Not on PyPI. Python 3.12+. Trainers stay in your repo. diff --git a/docs/interop/pettingzoo.md b/docs/interop/pettingzoo.md index 4110985..fb80d58 100644 --- a/docs/interop/pettingzoo.md +++ b/docs/interop/pettingzoo.md @@ -5,8 +5,8 @@ import netforge_rl import gymnasium as gym from pettingzoo import make -env = make("parallel", "netforge/ransomware-v4", max_ticks=200) -single = gym.make("NetForge/Blue-v4", max_ticks=200) +env = make('parallel', 'netforge/ransomware-v4', max_ticks=200) +single = gym.make('NetForge/Blue-v4', max_ticks=200) ``` | API | Id | diff --git a/docs/interop/soc_export.md b/docs/interop/soc_export.md index a9d6492..72329cf 100644 --- a/docs/interop/soc_export.md +++ b/docs/interop/soc_export.md @@ -6,7 +6,9 @@ from netforge_rl.environment.parallel_env import NetForgeRLEnv from netforge_rl.siem.export import export_ocsf, siem_to_ocsf -env = NetForgeRLEnv({'scenario_type': 'ransomware', 'max_ticks': 100, 'record_siem': True}) +env = NetForgeRLEnv( + {'scenario_type': 'ransomware', 'max_ticks': 100, 'record_siem': True} +) env.reset(seed=0) export_ocsf(env, 'runs/episode.ocsf.jsonl') siem_to_ocsf(log_line, subnet='10.0.0.0/24', tick=42) diff --git a/docs/python-quickstart.md b/docs/python-quickstart.md index 0105215..8164f36 100644 --- a/docs/python-quickstart.md +++ b/docs/python-quickstart.md @@ -13,12 +13,12 @@ import netforge_rl import gymnasium as gym from pettingzoo import make -env = make("parallel", "netforge/ransomware-v4", max_ticks=200) +env = make('parallel', 'netforge/ransomware-v4', max_ticks=200) obs, infos = env.reset(seed=0) actions = {agent: env.action_space(agent).sample() for agent in env.agents} obs, rewards, term, trunc, infos = env.step(actions) -blue = gym.make("NetForge/Blue-v4", max_ticks=200) +blue = gym.make('NetForge/Blue-v4', max_ticks=200) ``` | API | Id | | diff --git a/docs/submissions.md b/docs/submissions.md index 731615e..f1d7308 100644 --- a/docs/submissions.md +++ b/docs/submissions.md @@ -8,11 +8,13 @@ netforge evaluate examples/submissions/heuristic_blue.py --seeds 0 1 --max-ticks ``` ```python -NAME, TEAM, TECHNIQUE = "my-blue", "lab", "ppo-belief" +NAME, TEAM, TECHNIQUE = 'my-blue', 'lab', 'ppo-belief' + def wrap(env): return env # belief graphs already on; do not attach oracle_graph + def make_blue(seed=0): return MyPolicy(seed=seed) ``` diff --git a/docs/training/rllib.md b/docs/training/rllib.md index 974edc1..33cd69f 100644 --- a/docs/training/rllib.md +++ b/docs/training/rllib.md @@ -3,7 +3,7 @@ ```python from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv -env = NetForgeRLlibEnv({"scenario_type": "ransomware", "max_ticks": 100}) +env = NetForgeRLlibEnv({'scenario_type': 'ransomware', 'max_ticks': 100}) ``` Reference: `python benchmarks/rllib_rmappo.py` — shared LSTM PPO, red + blue, @@ -16,21 +16,21 @@ from ray.rllib.algorithms.ppo import PPOConfig from netforge_rl.bridges.rllib_bridge import NetForgeRLlibEnv ray.init() -register_env("netforge-v0", lambda cfg: NetForgeRLlibEnv(cfg)) +register_env('netforge-v0', lambda cfg: NetForgeRLlibEnv(cfg)) config = ( PPOConfig() - .environment("netforge-v0", env_config={"scenario_type": "ransomware"}) + .environment('netforge-v0', env_config={'scenario_type': 'ransomware'}) .api_stack( enable_rl_module_and_learner=False, enable_env_runner_and_connector_v2=False, ) .multi_agent( policies={ - "red_rmappo": (None, None, None, {"model": {"use_lstm": True}}), - "blue_rmappo": (None, None, None, {"model": {"use_lstm": True}}), + 'red_rmappo': (None, None, None, {'model': {'use_lstm': True}}), + 'blue_rmappo': (None, None, None, {'model': {'use_lstm': True}}), }, policy_mapping_fn=lambda agent_id, *a, **kw: ( - "red_rmappo" if "red" in agent_id else "blue_rmappo" + 'red_rmappo' if 'red' in agent_id else 'blue_rmappo' ), ) .training(train_batch_size=1024) diff --git a/docs/training/scenarios.md b/docs/training/scenarios.md index 650b4d2..9047f55 100644 --- a/docs/training/scenarios.md +++ b/docs/training/scenarios.md @@ -6,6 +6,7 @@ Subclass `BaseScenario`: `calculate_reward(agent_id, global_state, effect)` and ```python from netforge_rl.scenarios.base_scenario import BaseScenario + class CustomRansomware(BaseScenario): def calculate_reward(self, agent_id, global_state, effect=None): reward = 0.0 @@ -29,8 +30,7 @@ class CustomRansomware(BaseScenario): def _blue_reward(self, global_state, effect): hosts = global_state.all_hosts.values() healthy = sum( - 1 for h in hosts - if h.compromised_by == 'None' and h.status != 'isolated' + 1 for h in hosts if h.compromised_by == 'None' and h.status != 'isolated' ) return healthy / len(global_state.all_hosts)