Skip to content

Multi-Modal State Observability

Enable pcap_obs=True to extend each agent's observation dict with two additional arrays that expose the network state as synthesized packet traffic and a per-host feature matrix.

env = NetForgeRLEnv({
    'scenario_type': 'ransomware',
    'pcap_obs': True,
})
obs, _ = env.reset(seed=0)

Observation keys (when pcap_obs=True)

Key Shape dtype Description
pcap (32, 20) float32 Synthesized packet snapshot for this tick
node_features (100, 8) float32 Per-host attribute matrix for GNN models
adj_matrix (10000,) float32 100×100 routing adjacency (always present)

PCAP packet features (20 dims)

Idx Name Range Description
0 src_idx 0–1 Source host index / 100
1 dst_idx 0–1 Destination host index / 100
2 protocol 0–1 TCP=0.2 UDP=0.4 ICMP=0.6 Modbus=0.8 S7Comm=1.0
3 port_norm 0–1 Port / 65535
4 payload_kb 0–1 Payload size / 100 KB
5 flag_syn 0/1 TCP SYN
6 flag_rst 0/1 TCP RST
7 flag_ack 0/1 TCP ACK (C2 keepalive)
8 flag_psh 0/1 Data payload present
9 is_lateral 0/1 Source and destination in different subnets
10 is_c2 0/1 Beacon from compromised host
11 is_recon 0/1 ICMP/scan pattern
12 is_exfil 0/1 Large payload from compromised host
13 is_exploit 0/1 Toward known vulnerable port
14 dst_sensitive 0/1 Destination in Secure or OT subnet
15 src_privilege 0/0.5/1 None / User / Root
16 dst_compromised 0/1 Destination already owned
17 tick_norm 0–1 tick / max_ticks
18 is_encrypted 0/1 TLS/SSH (port 443 or 22)
19 severity 0/0.5/1 Benign / suspicious / critical

Node feature matrix (8 dims per host)

Idx Name Description
0 privilege 0=none 0.5=user 1.0=root
1 is_online host.status == 'online'
2 is_compromised compromised_by != 'None'
3 is_decoy decoy node
4 is_dc domain controller
5 subnet_type DMZ=0.2 Corp=0.4 Secure=0.6 OT=0.8
6 cvss_norm cvss_score / 10
7 edr_active endpoint detection running

GNN usage (PyTorch Geometric)

adj_matrix reshaped to (100, 100) is the adjacency matrix. Combined with node_features this is a complete graph:

import torch
from torch_geometric.data import Data

x = torch.tensor(obs['node_features'])               # (100, 8)
adj = torch.tensor(obs['adj_matrix'].reshape(100, 100))
edge_index = adj.nonzero().t().contiguous()           # (2, E)
data = Data(x=x, edge_index=edge_index)

Packet generation logic

Each tick the synthesizer derives packets from live simulation state — not random noise:

  1. C2 beacons — every compromised host generates a TCP/443 beacon toward the DMZ gateway
  2. Lateral movement — hosts with elevated privilege generate scan/exploit packets toward live targets
  3. Background — remaining slots fill with benign TCP/UDP traffic between online hosts

All values are in [0, 1] so no normalization is needed before passing to a neural network.