security-gym
Gymnasium-compatible replay environment for security defense research. Labeled log and kernel event streams recorded from a live vulnerable host are replayed without episode boundaries. The agent observes raw text (like tail -N on log files and kernel event channels) and takes defensive actions (block, throttle, alert, isolate) that causally suppress future observations.
Security-Gym is trace-driven: the telemetry is recorded, not produced by a dynamics model. The simulated component is the defense layer (blocklist, throttle list, isolation mode), which masks the stream in response to the agent's actions. The loop is therefore closed on observability but not on adversary behavior, since blocking an attacker suppresses that attacker's events while the recorded attacker does not change tactics in response. Throughout this repository, "environment" refers to the software and its Gymnasium API, "corpus" or "dataset" to the released v4.1 data, and "benchmark" to the evaluation protocol together with the baseline results in Baselines.
Built for the Alberta Plan vision of long-lived agents that continually learn from non-stationary sensory streams.
The continual-RL interaction loop. Telemetry sources (system logs and eBPF kernel events) are encoded as a Dict observation under one of two registered modes (Text or Hybrid). A streaming continual learner (no replay buffer; predict-then-update) emits a Dict action pairing a discrete defensive response with a continuous risk score, which updates a shared defense state (blocklist, throttle list, isolation mode). The dashed feedback edge captures the central difficulty of the domain: defensive actions causally suppress future observations on a continuous, non-episodic stream (terminated = False). The vector source is at figures/env-schematic.tex.
Dataset access — The v4 dataset is mirrored on HuggingFace (j-klawson/security-gym-v4, 2.8 GB compressed, 7d/30d/90d streams) and on Zenodo (10.5281/zenodo.18901541, 11.3 GB compressed, full release including the 365-day stream). MLCommons Croissant 1.0 metadata with Responsible AI fields ships at data/croissant.json.
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
Text Mode (SecurityLogStream-Text-v0)
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 kernel events
"network_events": Text # eBPF: connect/accept socket events
"file_events": Text # eBPF: open/unlink file events
"system_stats": Box(3) # [load_avg, mem_used_frac, disk_used_frac]
"defense_state": Text # Optional (defense_state_obs=True): own control state
})
Each text channel is a ring buffer of recent lines (configurable tail_lines and max_chars), updated on every step.
Hybrid Mode (SecurityLogStream-Hybrid-v0)
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
"defense_state": Box(6) # Optional (defense_state_obs=True)
})
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-Hybrid-v0", db_path="data/exp_7d_brute_v4.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
Defense State (defense_state_obs=True, opt-in)
Without this channel the environment reports only the host's telemetry, so an agent has no observable record of what it has already blocked, throttled, or isolated and must carry that state internally. An operator in the same position would run fail2ban-client status or iptables -L. Enabling defense_state_obs puts that answer in the observation.
Text mode renders it as command-style status output:
=== defense state ===
blocked (2): 10.0.0.5, 192.168.2.77
throttled (1): 198.51.100.2
isolation: off
events_suppressed: 1423
seconds_since_change: 84
current_src: 192.168.2.77 [BLOCKED]
Hybrid mode encodes the same facts as Box(6): [n_blocked, n_throttled, is_isolated, current_src_blocked, current_src_throttled, seconds_since_last_change]. The address list has no fixed-width encoding, so the two membership flags carry the part that bears on the current decision.
The current_src field is what makes de-escalation decidable from the observation rather than from agent memory: it reports whether the source of the event now being observed is already under a control. Every field is a consequence of the agent's own actions, so the channel is ground-truth-blind and leaks no label.
The channel is default-off, which leaves the legacy observation space byte-for-byte unchanged. Whether it becomes the published default is an open decision (see TODO.md).
env = gym.make("SecurityLogStream-Text-v0", db_path="data/exp_7d_brute_v4.db",
defense_state_obs=True, block_visibility="deny_log")
obs, info = env.reset()
print(obs["defense_state"])
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)
"target": Discrete(65) # 0=current event's source, 1..64=target slate slot
})
| 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) |
The agent can escalate and de-escalate: throttle -> block -> unblock.
Target selection
target names the address an action applies to. Index 0 is the current event's source, which was the only available binding before 0.6.0; indices 1 to target_slate_size name a slot in the target slate, an observable list of addresses seen anywhere in the stream.
This exists because binding actions to the current event forecloses most of the stream. 93.5% of exp_30d_heavy_v4 events carry no source address at all, and that is structural rather than a parsing gap: 99.9% of address-less events are eBPF, and an openat or execve has no peer address to record. Without target selection an agent that infers compromise from a suspicious execve cannot act on it, because the event it is looking at names no host. With it, the agent blocks the host it saw earlier in auth_log.
The slate is its own observation channel, since an index into a list the agent cannot see is not learnable. Text mode renders it as a numbered list; hybrid mode encodes Box(N, 3) as [addr_hash, is_blocked, is_throttled], hashing addresses with the same seed as the network channel's dst_ip_hash so a slate slot and a kernel event referring to one host hash alike.
=== targets ===
0 current_src: none
1 192.168.1.100 [BLOCKED]
2 10.0.0.5
3 -
Slots are stable, so a learned index keeps its referent. Eviction is least-recently-seen among addresses not under a control; blocked and throttled addresses are pinned, which is what keeps an already-blocked host nameable for release even under "drop" visibility, where its events stop arriving. If every slot is pinned, a newly seen address does not enter the slate and remains reachable through target 0 while it is the current event.
Two reward rules follow from targeting. An action that changes no state scores 0.0, rather than being paid in full for a block that dropped nothing. An action aimed at a slate address is graded on that address's last observed label rather than on whatever event is on screen, which is what makes it coherent to block a host named from kernel evidence while looking at an unrelated event; the grading uses only labels the agent has already been shown.
Set target_slate_size=0 to restore the pre-0.6.0 action space.
Block recovery (opt-in, default-off)
By default a blocked IP's events are dropped 100% and never reappear within an episode, so an unblock directed at it is unreachable (the IP is no longer observable). Two composable constructor parameters make the block then unblock loop learnable:
block_visibility="deny_log"(default"drop"): a blocked IP's events are still dropped by the firewall, but each is surfaced in the observation as a ground-truth-blind"[FIREWALL DENY] "line. The agent can see a wrongly-blocked benign IP still active andunblockit (the deny line makes that IP the current event), or see an attacker go quiet and leave it blocked. The deny rendering never encodesis_malicious, so no label leaks into the observation.block_ttl=<seconds>(defaultNone): fail2ban-style auto-expiry. A block lapses afterblock_ttlevent-seconds; the IP re-surfaces for a fresh decision.
env = gym.make("SecurityLogStream-Text-v0", db_path=..., block_visibility="deny_log", block_ttl=300.0)
Reward Function
Two components by default (an action term and ongoing consequences), plus an optional risk-score term:
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 (optional, disabled by default; enable via reward_config={"include_risk_reward": True}): -0.1 * (predicted_risk - true_risk)^2 — penalizes inaccurate threat assessment. It is off by default because, as a per-event term that accumulates against every observed event, it can make observation-suppressing actions (block/throttle/isolate) rationally dominant.
Ongoing consequences: blocked/throttled events accumulate reward between steps (+0.1 per blocked attack event, -0.5 per blocked benign event). The agent feels the sustained cost of false positives.
Under block_visibility="deny_log", a surfaced deny-log entry contributes only the consequence term: the per-step action and risk terms are zeroed so the event is not double-counted (the firewall, not the agent's live decision, is acting on it). The agent's action on a denied event is graded on the next live event it produces. Episode-total consequence reward is unchanged from "drop" mode; "deny_log" only redistributes it across steps (one step per blocked event) and lengthens the episode.
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. The remaining three tactics are planned and have no module, config, or data.
Campaign Configurations
Nine YAML configs in campaigns/ drive the six modules. Six are single-phase, isolating one module so its signature can be studied on its own; three chain multiple phases with distinct timing profiles per phase.
| Config | Phases | Notes |
|---|---|---|
recon_only.yaml |
recon | SYN scan from spoofed source IPs |
ssh_brute_only.yaml |
ssh_brute_force | Aliased IPs, accelerating timing profile |
credential_stuffing_only.yaml |
credential_stuffing | Unique pairs tried once each |
log4shell_only.yaml |
log4shell | Decelerating profile (spray, then slow to evade) |
redis_exploit_only.yaml |
redis_lua_escape | Custom profile: slow enum, fast exploit, slow post-exploit |
post_auth_only.yaml |
ssh_post_auth | Valid credentials, system profiling commands |
recon_ssh_log4shell.yaml |
recon -> ssh_brute_force -> log4shell | Largest campaign in the dataset |
full_killchain.yaml |
recon -> credential_stuffing -> ssh_post_auth | Ends in a successful shell |
redis_killchain.yaml |
recon -> redis_lua_escape -> ssh_post_auth | Redis RCE, then SSH pivot |
Run one with python -m attacks run campaigns/<config>.yaml (see Running Attack Campaigns). The shipped campaigns_v2.db holds the recorded output of these configs: 60,468 events, of which 30,436 are labeled malicious by time and source-IP matching. The remainder is target-host background and collector activity captured inside the same collection windows, not a benign traffic baseline; the benign baseline comes from benign_v4.db at composition time. Per-campaign counts and known limitations are documented in data/DATASET_README.md.
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.
Baselines
Baseline agents establish performance bounds for the environment. See examples/ for runnable scripts. Results below are from exp_30d_heavy_v4.db (1M steps, seed 42, all 5 attack types), regenerated for 0.6.0.
| Agent | Description | Precision | Recall | F1 | Mean Reward |
|---|---|---|---|---|---|
| pass-only | Never acts — always passes | 0.000 | 0.000 | 0.000 | -0.0120 |
| random | Samples the full action space | 0.005 | 0.675 | 0.010 | -0.4061 |
| threshold(5) | Block IP after 5 failed SSH auths in 5 min | 1.000 | 0.005 | 0.011 | +0.0001 |
| keyword | Multi-channel SIEM-style pattern matching | 0.987 | 0.013 | 0.025 | -0.0115 |
| rlsecd | 5-head MLP continual learner (rlsecd) | 0.979 | 0.979 | 0.979 | — |
Reported under the 0.6.0 defaults: include_risk_reward=False, target_slate_size=64, defense_state_obs=True. The mean-reward column moved substantially from the pre-0.6.0 table for two independent reasons, and the detection columns barely moved at all, since they never depended on the reward.
The larger reason predates this release. The previous table was generated while include_risk_reward still defaulted to True, and was not regenerated when commit a5ceae9 flipped that default off. Its pass-only figure of -0.073 is reproduced exactly by adding the dormant risk-MSE term back (-0.0733 computed directly from the first 1M events), so that column had been describing a reward function the environment no longer computed. The smaller reason is this release: an action that changes no state now scores 0.0 instead of being paid in full, which matters because ~93.5% of steps carry no address for an action to bind to.
Both heuristic agents achieve high precision but near-zero recall. The threshold agent only sees SSH brute force in auth_log text. The keyword agent adds rules for Log4Shell, process ancestry, and file access across all channels, doubling F1 — but still catches only 1.3% of attacks because most malicious events are eBPF kernel telemetry (file opens, process exits, network accepts) that don't match static signatures. Only a learning agent can generalize across the full observation space.
# Run all baselines on an experiment stream
python examples/benchmark.py data/exp_7d_brute_v4.db
# Individual agents
python examples/random_agent.py data/exp_7d_brute_v4.db
python examples/threshold_agent.py data/exp_7d_brute_v4.db --threshold 5
python examples/keyword_agent.py data/exp_7d_brute_v4.db
python examples/streaming_demo.py data/exp_7d_brute_v4.db --mode gym
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 and eBPF kernel events) are available from Zenodo and GitHub Releases.
The v4 dataset includes 11.2M benign events (7.9M logs + 3.24M eBPF from 3 servers) and 60K attack events across 5 attack types. Pre-composed experiment streams range from 4.9M events (7-day) to 257.7M events (365-day).
Download the latest dataset:
# Via CLI (after pip install)
security-gym download
# Or list available releases first
security-gym list
Or download from Zenodo and decompress with zstd -d <file>.zst into data/.
Quick Start
Basic Gymnasium Usage
import gymnasium as gym
import numpy as np
import security_gym
env = gym.make("SecurityLogStream-Text-v0", db_path="data/exp_7d_brute_v4.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.1 per event (confirmed mitigation)
- Dropped benign events: -0.5 per event (service impact)
ANSI Rendering
env = gym.make("SecurityLogStream-Text-v0", db_path="data/exp_7d_brute_v4.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/exp_7d_brute_v4.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/exp_7d_brute_v4.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 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 v4 benign dataset includes 3.24M eBPF events from 24-hour collections on 3 servers.
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_v4.db
ebpf_sample_rate: 0.242 # 24.2%: approximates a single host's event rate
attacks:
db: data/campaigns_v2.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/exp_90d_v4.db
Project Structure
security-gym/
├── src/security_gym/ # Installable package
│ ├── adapters/ # SecurityGymStream (batch/streaming adapter)
│ ├── data/ # EventStore (SQLite), StreamComposer
│ ├── envs/ # SecurityLogStreamEnv (v1, v2), deprecated wrappers
│ ├── features/ # Deprecated (v0 numeric extractors)
│ ├── parsers/ # auth_log, syslog, web_access, web_error, journal, ebpf
│ └── targets/ # Deprecated (v0 multi-head target builder)
├── examples/ # Baseline agents and usage demos
├── 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 (339 tests)
pytest attacks/tests/ # Attack framework tests (120 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
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.6.1.tar.gz.
File metadata
- Download URL: security_gym-0.6.1.tar.gz
- Upload date:
- Size: 487.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b48beff56fbf05b0c3ebd1b1c48c4eb243e5b86c6cc2dcee55fe9ca61b969c35
|
|
| MD5 |
461ac798df6a42d96c619c3b41ae503a
|
|
| BLAKE2b-256 |
3e7a7b2610a08af250ffa4b1aa8aa38f2498d488501d236516a96416644ad9c6
|
Provenance
The following attestation bundles were made for security_gym-0.6.1.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.6.1.tar.gz -
Subject digest:
b48beff56fbf05b0c3ebd1b1c48c4eb243e5b86c6cc2dcee55fe9ca61b969c35 - Sigstore transparency entry: 2675871249
- Sigstore integration time:
-
Permalink:
j-klawson/security-gym@9c462152e4758cc8dd7b6850db7505c8cec7eb1c -
Branch / Tag:
refs/tags/v0.6.1 - Owner: https://github.com/j-klawson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9c462152e4758cc8dd7b6850db7505c8cec7eb1c -
Trigger Event:
push
-
Statement type:
File details
Details for the file security_gym-0.6.1-py3-none-any.whl.
File metadata
- Download URL: security_gym-0.6.1-py3-none-any.whl
- Upload date:
- Size: 75.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f27f21e63111eeb698e21384ccda4fa15d6964c1dd1df9f1584aacbae1881d1b
|
|
| MD5 |
cce3cc7faa1e4219dc8fac3fbf6a834c
|
|
| BLAKE2b-256 |
05b8b156fbcd2db47b2449a404a2be2b9c239a1947c8ed72073a1886674bcab3
|
Provenance
The following attestation bundles were made for security_gym-0.6.1-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.6.1-py3-none-any.whl -
Subject digest:
f27f21e63111eeb698e21384ccda4fa15d6964c1dd1df9f1584aacbae1881d1b - Sigstore transparency entry: 2675871289
- Sigstore integration time:
-
Permalink:
j-klawson/security-gym@9c462152e4758cc8dd7b6850db7505c8cec7eb1c -
Branch / Tag:
refs/tags/v0.6.1 - Owner: https://github.com/j-klawson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9c462152e4758cc8dd7b6850db7505c8cec7eb1c -
Trigger Event:
push
-
Statement type: