Gymnasium environments for cybersecurity threat detection with continual learning
Project description
security-gym
Gymnasium-compatible environment for security defense research. The agent observes raw text streams — like tail -N on log files and kernel event channels — and takes defensive actions (block, throttle, alert, isolate) that causally affect future observations.
Built for the Alberta Plan vision of long-lived agents that continually learn from non-stationary sensory streams.
Features
- Raw text observations (v1) — 6 text channels (auth_log, syslog, web_log, process_events, network_events, file_events) + numeric system stats. The agent learns its own representations.
- Hybrid text + structured observations (v2) — 3 text channels for logs + 3 fixed-width float32 arrays for eBPF kernel events. Matches how real SOC tooling consumes data: text for human-readable logs, structured arrays for kernel telemetry.
- Defensive action space — 6 actions (pass / alert / throttle / block_source / unblock / isolate) + continuous risk score. Actions causally affect future observations.
- Asymmetric rewards — blocking an attacker earns +1.0, blocking a legitimate user costs -1.0. Ongoing consequence feedback from blocked/throttled events accumulates between steps.
- Continuous stream —
terminatedis alwaysFalse; the log stream never ends (just like a real server) - eBPF kernel events — process execution, network connections, and file access captured via BPF tracepoints. Mirrors how modern EDR agents work.
- Attack framework — YAML-driven campaign orchestrator with 6 modules: SSH brute force, credential stuffing, Log4Shell, Redis Lua sandbox escape (CVE-2022-0543), port scan, post-auth execution
- Stream composition — offline mixing of benign + attack data with Poisson-scheduled campaigns and MITRE ATT&CK-weighted type distributions
Observation Space
V1 — All Text (SecurityLogStream-v1)
The agent sees the same data a security analyst would — raw log files and kernel event streams:
Dict({
"auth_log": Text # SSH auth events (tail of /var/log/auth.log)
"syslog": Text # System events (tail of /var/log/syslog)
"web_log": Text # Combined web access/error logs
"process_events": Text # eBPF: execve/exit/fork kernel events
"network_events": Text # eBPF: connect/accept/bind socket events
"file_events": Text # eBPF: open/write/unlink file events
"system_stats": Box(3) # [load_avg, mem_used_frac, disk_used_frac]
})
Each text channel is a ring buffer of recent lines (configurable tail_lines and max_chars), updated on every step.
V2 — Hybrid Text + Structured (SecurityLogStream-v2)
Log channels remain as text; eBPF kernel events become fixed-width float32 arrays:
Dict({
"auth_log": Text # Unchanged — raw log text
"syslog": Text # Unchanged
"web_log": Text # Unchanged
"process_events": Box(50, 8) # [log_dt, pid, ppid, uid, syscall, comm_hash, parent_hash, tree_depth]
"network_events": Box(50, 7) # [log_dt, pid, uid, syscall, dst_ip_hash, dst_port, comm_hash]
"file_events": Box(50, 6) # [log_dt, pid, uid, syscall, flags, path_hash]
"system_stats": Box(3) # Unchanged
})
Each structured channel is a ring buffer of tail_events rows (default 50). String fields (comm, IP, path) are hashed via mmh3 with per-field seeds. Timestamp deltas are log-scaled (log(1 + dt)) for gradient stability. Process events track tree depth from pid/ppid ancestry.
env = gym.make("SecurityLogStream-v2", db_path="data/campaigns.db", tail_events=50)
obs, info = env.reset()
print(obs["auth_log"][:100]) # str — raw log text
print(obs["process_events"].shape) # (50, 8) — float32 array
Action Space
Dict({
"action": Discrete(6) # 0=pass, 1=alert, 2=throttle, 3=block_source, 4=unblock, 5=isolate
"risk_score": Box(0, 10) # Agent's estimate of current threat level (auxiliary prediction)
})
| Action | Effect |
|---|---|
pass |
Continue monitoring |
alert |
Flag for human review |
throttle |
Rate-limit source IP (~90% drop) |
block_source |
Add source IP to firewall blocklist (100% drop) |
unblock |
Remove source IP from blocklist/throttle list |
isolate |
Quarantine server (block all network events) |
IP-targeted actions use the current event's source IP. The agent can escalate and de-escalate: throttle -> block -> unblock.
Reward Function
Three components combined:
Action reward (asymmetric — mistakes in both directions are costly):
| Action | During Attack | During Benign |
|---|---|---|
block_source |
+1.0 | -1.0 |
throttle |
+0.75 | -0.5 |
alert |
+0.5 | -0.3 |
pass |
-0.5 | 0.0 |
isolate |
+0.25 | -2.0 |
unblock |
-0.5 | 0.0 |
Risk score MSE: -0.1 * (predicted_risk - true_risk)^2 — penalizes inaccurate threat assessment.
Ongoing consequences: blocked/throttled events accumulate reward between steps (+0.05 per blocked attack event, -0.1 per blocked benign event). The agent feels the sustained cost of false positives.
Supported Attacks
| Attack Type | Module | MITRE Technique | MITRE Tactic | Description |
|---|---|---|---|---|
discovery |
recon |
T1046 — Network Service Discovery | TA0007 — Discovery | SYN port scan via scapy raw sockets |
brute_force |
ssh_brute_force |
T1110.001 — Password Guessing | TA0006 — Credential Access | SSH password brute force via paramiko with IP aliasing |
web_exploit |
log4shell |
T1190 — Exploit Public-Facing Application | TA0001 — Initial Access | Log4Shell (CVE-2021-44228) JNDI injection via HTTP |
credential_stuffing |
credential_stuffing |
T1110.004 — Credential Stuffing | TA0006 — Credential Access | Breach dump credentials, each tried once via SSH |
web_exploit |
redis_lua_escape |
T1190 — Exploit Public-Facing Application | TA0001 — Initial Access | Redis Lua sandbox escape (CVE-2022-0543, CVSS 10.0) — 3-stage: enum → Lua sandbox escape via package.loadlib() → post-exploit RCE |
execution |
ssh_post_auth |
T1059.004 — Unix Shell | TA0002 — Execution | Post-auth command execution + optional payload download |
persistence |
— | — | TA0003 — Persistence | Planned |
privilege_escalation |
— | — | TA0004 — Privilege Escalation | Planned |
exfiltration |
— | — | TA0010 — Exfiltration | Planned |
The first six attacks have implemented modules, campaign configs, and validated datasets. Two kill chain campaigns combine multiple phases: recon -> credential stuffing -> post-auth execution, and recon -> Redis exploit -> SSH pivot.
Redis Lua Sandbox Escape (CVE-2022-0543)
The redis_lua_escape module exploits a Debian-specific vulnerability where Redis is dynamically linked against liblua5.1, allowing package.loadlib() to escape the Lua sandbox for unauthenticated RCE. The attack runs in three stages:
- Enumeration — fingerprint Redis via
INFO,CONFIG GET *,DBSIZE,CLIENT LIST - Exploitation — Lua sandbox escape via
EVAL+package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io") - Post-exploitation — system commands via repeated
EVALcalls (id,whoami, then configurable command profiles)
Key eBPF detection signal: execve events where parent_comm=redis-server — Redis spawning shell commands (sh, bash, id, cat) is highly anomalous. The eBPF collector captures ppid + parent_comm on every process event, so this parent-child relationship appears directly in the process_events text channel.
Install
pip install security-gym
Or from source:
git clone https://github.com/j-klawson/security-gym.git
cd security-gym
pip install -e ".[dev]"
Optional extras:
pip install -e ".[alberta]" # JAX + alberta-framework for RL experiments
pip install -e ".[attacks]" # paramiko, requests, scapy for attack generation
pip install -e ".[all]" # Everything
Dataset
Pre-built datasets (SQLite databases with labeled log events) are available from GitHub Releases and archived on Zenodo.
Download the latest dataset:
# Via CLI (after pip install)
security-gym download
# Or list available releases first
security-gym list
Or manually download campaigns.db from the Releases page and place it in data/.
Quick Start
Basic Gymnasium Usage
import gymnasium as gym
import numpy as np
import security_gym
env = gym.make("SecurityLogStream-v1", db_path="data/campaigns.db")
obs, info = env.reset()
# obs is a dict of text channels + system stats
print(obs["auth_log"][:200]) # Raw auth log lines
print(obs["system_stats"]) # [load_avg, mem_used, disk_used]
while True:
# Choose an action
action = {
"action": 0, # pass (monitor only)
"risk_score": np.array([0.0], dtype=np.float32),
}
obs, reward, terminated, truncated, info = env.step(action)
# Ground truth (for evaluation, not visible to agent)
gt = info["ground_truth"]
print(f"{info['timestamp']} | malicious={gt['is_malicious']} | "
f"risk={gt['true_risk']:.1f} | reward={reward:.2f}")
if truncated: # End of data
break
Defensive Actions
import numpy as np
# Block the current event's source IP (100% drop)
block = {"action": 3, "risk_score": np.array([8.0], dtype=np.float32)}
# Throttle (90% drop rate)
throttle = {"action": 2, "risk_score": np.array([5.0], dtype=np.float32)}
# Alert with high risk estimate
alert = {"action": 1, "risk_score": np.array([7.0], dtype=np.float32)}
# Undo a block (correct false positive)
unblock = {"action": 4, "risk_score": np.array([1.0], dtype=np.float32)}
# Quarantine server (blocks all network events)
isolate = {"action": 5, "risk_score": np.array([10.0], dtype=np.float32)}
After blocking an IP, future events from that IP are silently dropped. The agent observes the absence of those events and receives ongoing consequence feedback:
- Dropped attack events: +0.05 per event (confirmed mitigation)
- Dropped benign events: -0.1 per event (service impact)
ANSI Rendering
env = gym.make("SecurityLogStream-v1", db_path="data/campaigns.db", render_mode="ansi")
obs, info = env.reset()
for _ in range(20):
action = {"action": 0, "risk_score": np.array([0.0], dtype=np.float32)}
obs, reward, terminated, truncated, info = env.step(action)
print(env.render()) # Color-coded: red=malicious, green=benign
SecurityGymStream (Batch/Streaming Adapter)
For direct integration with learning frameworks (bypasses Gymnasium overhead):
from security_gym.adapters.scan_stream import SecurityGymStream
stream = SecurityGymStream("data/campaigns.db")
# Batch: load all observations and ground truth
observations, ground_truths = stream.collect_numpy()
# observations: list of dicts (one per event, each with text channels + system_stats)
# ground_truths: list of dicts (is_malicious, attack_type, true_risk, ...)
# Constant-memory streaming
for obs_batch, gt_batch in stream.iter_batches(size=1000):
for obs, gt in zip(obs_batch, gt_batch):
print(obs["auth_log"][:80], gt["is_malicious"])
# Server-speed evaluation mode (never-ending, paced stream)
stream = SecurityGymStream("data/campaigns.db", speed=10.0, loop=True)
for timestep in stream: # Requires JAX
...
Generating Data
Running Attack Campaigns
The attack framework generates labeled data by executing scripted attacks against a target VM and collecting the resulting logs:
# List available attack modules
python -m attacks list-modules
# Validate a campaign config
python -m attacks validate campaigns/ssh_brute_only.yaml
# Dry run (preview without executing)
python -m attacks run campaigns/ssh_brute_only.yaml --dry-run
# Execute (requires network access to target VM)
sudo python -m attacks run campaigns/ssh_brute_only.yaml
Campaign configs are YAML files defining attack phases, timing profiles, IP strategies, and log collection:
campaign:
name: "SSH Brute Force Only"
seed: 42
target:
host: 192.168.2.201
ssh_user: researcher
ssh_key: ~/.ssh/your_public_key
collection:
ebpf:
enabled: true # Collect kernel events via eBPF
phases:
- name: "SSH Brute Force"
module: ssh_brute_force
mitre_technique: "T1110.001"
params:
usernames: ["root", "admin", "ubuntu"]
passwords: ["password", "123456", "admin"]
target_port: 22
max_attempts_per_ip: 10
ip_source:
strategy: aliased
count: 5
subnet: "192.168.2.0/24"
timing:
duration_seconds: 300
profile: constant
jitter_ms: [200, 800]
Importing Benign Logs
Import real server logs as baseline benign data:
python -m attacks import-logs server_logs.tar --db data/benign.db --host myserver
Collecting eBPF Kernel Events
The three kernel observation channels (process_events, network_events, file_events) are populated by an eBPF collector daemon that attaches to Linux kernel tracepoints via BCC. This captures syscall-level activity invisible to traditional log files — the agent sees process execution chains, network connections, and file access as they happen in the kernel.
What's captured:
| Channel | Tracepoints | Fields |
|---|---|---|
process_events |
sys_enter_execve, sched_process_exit |
pid, ppid, uid, comm, parent_comm, args, exit code |
network_events |
sys_enter_connect, sys_enter_accept4 |
pid, uid, comm, dst IP:port |
file_events |
sys_enter_openat, sys_enter_unlinkat |
pid, comm, path, flags |
Process events include parent process ancestry (ppid + parent_comm), allowing the agent to learn causal chains — e.g., apache2 → wget is suspicious while cron → wget may be routine. Network events include the effective UID, so the agent can learn user-identity-aware policies.
Benign baseline collection:
eBPF kernel events are collected from the target server during normal operation (no attacks running) to establish a baseline of benign system activity:
# Collect 1 hour of benign kernel events from the target server
python scripts/collect_ebpf_baseline.py --duration 3600
# Preview without collecting
python scripts/collect_ebpf_baseline.py --duration 3600 --dry-run
This copies the existing benign log database to a v2 database, then SSHs into the target, deploys the eBPF collector, runs for the specified duration, retrieves the events, and inserts them as benign (is_malicious=0). The resulting benign_v2.db contains both traditional log events and kernel-level events.
During attack campaigns:
When ebpf: {enabled: true} is set in a campaign YAML, the orchestrator automatically starts the eBPF collector before the attack begins and stops it after. Kernel events captured during attack windows are labeled malicious through the same time+IP matching used for log events — an execve wget from the attacker's IP during an attack phase is correctly labeled as part of the attack.
Composing Experiment Streams
Combine benign and attack data into reproducible experiment streams:
# Preview composition plan
python -m attacks compose configs/stream_90d_mixed.yaml --dry-run
# Generate composed stream
python -m attacks compose configs/stream_90d_mixed.yaml
Composition configs control duration, attack frequency, and MITRE ATT&CK-weighted type distributions:
stream:
duration: 90d
seed: 42
benign:
db: data/benign.db
attacks:
db: data/campaigns.db
campaigns_per_day: 3.0
distribution:
discovery: 0.35
brute_force: 0.30
web_exploit: 0.20
credential_stuffing: 0.10
execution: 0.05
output:
db: data/exp01_90d.db
Project Structure
security-gym/
├── src/security_gym/ # Installable package
│ ├── adapters/ # SecurityGymStream (batch/streaming adapter)
│ ├── data/ # EventStore (SQLite), StreamComposer
│ ├── envs/ # SecurityLogStreamEnv (v1), deprecated wrappers
│ ├── features/ # Deprecated (v0 numeric extractors)
│ ├── parsers/ # auth_log, syslog, web_access, web_error, journal, ebpf
│ └── targets/ # Deprecated (v0 multi-head target builder)
├── attacks/ # Attack framework (NOT pip-installed)
│ ├── modules/ # recon, ssh_brute_force, credential_stuffing, ssh_post_auth, log4shell, redis_lua_escape
│ ├── collection/ # SSH/SFTP log collector, benign log importer, eBPF orchestrator
│ ├── labeling/ # Time+IP campaign labeler
│ └── tests/ # Attack framework tests
├── campaigns/ # YAML campaign configs
├── configs/ # YAML composition configs
├── server/ # Target VM provisioning docs, eBPF collector daemon
└── tests/ # Core package tests
Development
pip install -e ".[dev]"
pytest tests/ # Core tests (227 tests)
pytest attacks/tests/ # Attack framework tests (90 tests)
ruff check src/ tests/ attacks/ # Lint
Requirements
- Python >= 3.11
- gymnasium >= 1.0.0
- numpy >= 1.24.0
Author
Keith Lawson
License
Apache-2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file security_gym-0.3.6.tar.gz.
File metadata
- Download URL: security_gym-0.3.6.tar.gz
- Upload date:
- Size: 159.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4ed722bf7e08efb4077741f1a77a53ea202008845f7bd23f52cf12c1b10185b
|
|
| MD5 |
b543150849fa6069ec5f6647df3d7a78
|
|
| BLAKE2b-256 |
6037fddc6d74439dabf4ae6eb999ea191cabbc0a66eca67a82e772ce0f250276
|
Provenance
The following attestation bundles were made for security_gym-0.3.6.tar.gz:
Publisher:
publish.yml on j-klawson/security-gym
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
security_gym-0.3.6.tar.gz -
Subject digest:
c4ed722bf7e08efb4077741f1a77a53ea202008845f7bd23f52cf12c1b10185b - Sigstore transparency entry: 1147910392
- Sigstore integration time:
-
Permalink:
j-klawson/security-gym@b48a50ea07846e31b81b111f5434c4ca7c566c9d -
Branch / Tag:
refs/tags/v0.3.6 - Owner: https://github.com/j-klawson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b48a50ea07846e31b81b111f5434c4ca7c566c9d -
Trigger Event:
push
-
Statement type:
File details
Details for the file security_gym-0.3.6-py3-none-any.whl.
File metadata
- Download URL: security_gym-0.3.6-py3-none-any.whl
- Upload date:
- Size: 59.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a7795072148db0719d8ad887d0c552c22a511752b5594d421a5278f1fffd305
|
|
| MD5 |
e9782b915c946e31cf7eb74e1c487989
|
|
| BLAKE2b-256 |
b4b62d89cda7cd024b83b8cc0f7643e00377d29bfcda1b6b511230a6ff0f73b3
|
Provenance
The following attestation bundles were made for security_gym-0.3.6-py3-none-any.whl:
Publisher:
publish.yml on j-klawson/security-gym
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
security_gym-0.3.6-py3-none-any.whl -
Subject digest:
8a7795072148db0719d8ad887d0c552c22a511752b5594d421a5278f1fffd305 - Sigstore transparency entry: 1147910398
- Sigstore integration time:
-
Permalink:
j-klawson/security-gym@b48a50ea07846e31b81b111f5434c4ca7c566c9d -
Branch / Tag:
refs/tags/v0.3.6 - Owner: https://github.com/j-klawson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b48a50ea07846e31b81b111f5434c4ca7c566c9d -
Trigger Event:
push
-
Statement type: