Part of the Nebula Forge security tools suite.
AtomicLoop closes the purple team validation loop: simulate an attack technique, capture endpoint events, and immediately validate whether your Sigma/Wazuh rules fire. No need for the full Atomic Red Team framework.
Write Sigma rule → Simulate attack (AtomicLoop) → Capture events (LogNorm)
→ Validate detection (DriftWatch) → Fix gap → Repeat
purple-loop:
AtomicLoop → LogNorm → ClusterIQ → HuntForge → DriftWatch → repeat
- 20 embedded MITRE ATT&CK techniques — curated tests for T1059.001 through T1190, no internet or framework required
- Safety controls — dry_run preview + explicit
confirmflag prevents accidental execution - Local execution — PowerShell, cmd, and bash executors with configurable timeout
- WinRM remote execution — run atomic tests on remote Windows hosts via PS Remoting (T1021.006); optional credential support
- Event capture — reads Windows Security + Sysmon event logs during the test window
- LogNorm integration — normalizes captured events to ECS-lite format (port 5006)
- DriftWatch integration — validates Sigma rules against captured events (port 5008)
- Gap analysis — explains exactly why a detection fired or missed
- Persistent history — SQLite session library with search, export, and delete
- CLI — offline operation without the web UI
cd AtomicLoop
pip install -r requirements.txt
cp config.example.yaml config.yaml # optional
python app.pyOpen - 127.0.0.1:5011
- Browse techniques in the left panel (grouped by tactic).
- Click a technique to expand its test list.
- Select a test to see command preview, expected artifacts, and input arguments.
- Toggle Dry Run to preview the command without executing.
- When ready: disable Dry Run, check the confirm checkbox, set timeout.
- Click Execute Test — results appear in the right panel.
- Paste a Sigma rule in the Detection Validation panel and click Validate Detection.
# List all techniques
python cli.py --list
# Show tests for a technique
python cli.py --technique T1059.001
# Dry run (preview command only)
python cli.py --technique T1059.001 --test 1 --dry-run
# Execute with confirmation
python cli.py --technique T1059.001 --test 1 --confirm
# Execute and validate against a Sigma rule
python cli.py --technique T1059.001 --test 1 --confirm --validate --sigma rule.yml
# Custom input arguments
python cli.py --technique T1059.001 --test 2 --confirm --arg target_url=http://127.0.0.1:8080
# Save output to file
python cli.py --technique T1059.001 --test 1 --confirm --output result.md
# List saved runs
python cli.py --results| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/atomics |
List all techniques |
| GET | /api/atomics/<technique_id> |
Get tests for a technique |
| POST | /api/run |
Execute an atomic test (engine path) |
| POST | /api/validate |
Validate Sigma rule against events |
| GET | /api/results |
List past runs (paginated) |
| GET | /api/result/<run_id> |
Get a single run |
| DELETE | /api/result/<run_id> |
Delete a run |
| GET | /api/result/<run_id>/export |
Export run (JSON or Markdown) |
| POST | /execute |
Direct command execution — local or WinRM remote (API key protected) |
{
"technique_id": "T1059.001",
"test_number": 1,
"confirm": true,
"dry_run": false,
"capture_events": true,
"normalize": true,
"timeout": 30,
"input_arguments": {"target_url": "http://127.0.0.1:8080"}
}Response:
{
"success": true,
"run_id": "uuid",
"technique_id": "T1059.001",
"test_name": "PowerShell Encoded Command Execution",
"executed_at": "2025-01-01T12:00:00Z",
"exit_code": 0,
"duration_ms": 1240,
"event_count": 12,
"events": [{...ECS-lite...}],
"raw_output": "AtomicTest T1059.001-1: Encoded execution"
}Executes an allowlisted atomic command directly — locally or on a remote Windows host via WinRM. Protected by ATOMICLOOP_API_KEY when that env var is set (see Environment variables).
Request headers (when API key is configured):
X-API-Key: <your-key>
Content-Type: application/json
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
command |
string | Yes | — | Atomic test command to execute. Must match a command in the embedded allowlist. |
executor_type |
string | No | "powershell" |
Executor used for allowlist lookup and local dispatch: powershell, cmd, bash. |
target_host |
string | Conditional | — | Remote host (hostname or IPv4/IPv6). Required when transport is "winrm". |
transport |
string | No | — | Set to "winrm" to execute on target_host via PS Remoting. Omit for local execution. |
credential |
object | No | — | {"username": "DOMAIN\\user", "password": "secret"}. Passed as -Credential to New-PSSession. Only used when transport is "winrm". |
timeout |
integer | No | 30 |
Seconds before the process or remote session is killed. |
dry_run |
boolean | No | false |
If true, returns the command that would be run without executing it. |
Local execution example:
{
"command": "Get-Process",
"executor_type": "powershell",
"timeout": 30,
"dry_run": false
}WinRM remote execution example:
{
"command": "Get-Process",
"executor_type": "powershell",
"target_host": "192.168.1.50",
"transport": "winrm",
"credential": {"username": "CORP\\svctest", "password": "hunter2"},
"timeout": 60,
"dry_run": false
}Dry-run example (no execution, no API key required logic applies normally):
{
"command": "Get-Process",
"target_host": "192.168.1.50",
"transport": "winrm",
"dry_run": true
}Response:
{
"success": true,
"exit_code": 0,
"stdout": "...",
"stderr": "",
"duration_ms": 1340,
"timed_out": false,
"dry_run": false,
"command": "Get-Process",
"error": null
}Error responses:
| Status | Body | Cause |
|---|---|---|
400 |
{"success": false, "error": "command is required"} |
Empty or missing command field |
400 |
{"success": false, "error": "target_host is required when transport is 'winrm'"} |
transport=winrm with no target_host |
200 |
{"success": false, "error": "Command is not in the embedded atomic allowlist."} |
Command does not match any embedded atomic test |
200 |
{"success": false, "error": "Invalid target_host: ..."} |
target_host contains characters outside hostname/IP character set |
401 |
{"error": "unauthorized"} |
X-API-Key header missing or incorrect |
{
"run_id": "uuid",
"sigma_rule": "title: Detect PowerShell Encoded Command\ndetection:\n ..."
}Response:
{
"success": true,
"detection_fired": true,
"matched_events": [{...}],
"match_count": 3,
"gap_analysis": "Validated via DriftWatch. Detection FIRED: Sigma rule matched 3 of 12 captured events.",
"source": "driftwatch"
}| Technique | Name | Tactic |
|---|---|---|
| T1059.001 | PowerShell | Execution |
| T1059.003 | Windows Command Shell | Execution |
| T1055 | Process Injection | Defense Evasion |
| T1003 | OS Credential Dumping | Credential Access |
| T1082 | System Information Discovery | Discovery |
| T1083 | File and Directory Discovery | Discovery |
| T1057 | Process Discovery | Discovery |
| T1069 | Permission Groups Discovery | Discovery |
| T1021.001 | Remote Desktop Protocol | Lateral Movement |
| T1021.002 | SMB/Windows Admin Shares | Lateral Movement |
| T1547.001 | Registry Run Keys | Persistence |
| T1053.005 | Scheduled Task | Persistence |
| T1070.001 | Clear Windows Event Logs | Defense Evasion |
| T1112 | Modify Registry | Defense Evasion |
| T1027 | Obfuscated Files | Defense Evasion |
| T1562.001 | Impair Defenses | Defense Evasion |
| T1566.001 | Spearphishing Attachment | Initial Access |
| T1078 | Valid Accounts | Defense Evasion |
| T1110.001 | Password Guessing | Credential Access |
| T1190 | Exploit Public-Facing Application | Initial Access |
The /execute route with transport=winrm uses PowerShell Remoting (New-PSSession / Invoke-Command). The following must be true on the target host before remote execution will succeed.
# Enable PS Remoting (run as Administrator)
Enable-PSRemoting -Force
# Confirm WinRM is listening
winrm enumerate winrm/config/listener
# If the source host is not domain-joined, add it to TrustedHosts on the source
# (run on the AtomicLoop host, not the target)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "192.168.1.50" -Force| Port | Protocol | Direction | Purpose |
|---|---|---|---|
| 5985 | HTTP | Source → Target | WinRM (unencrypted, lab use) |
| 5986 | HTTPS | Source → Target | WinRM over TLS (recommended for non-lab) |
Pass credentials via the credential field in the request body. The account must have permission to create PS sessions on the target (local Administrator or a delegated WinRM user).
Note: Credentials are transmitted in the JSON request body and embedded in a PowerShell
-Commandstring. Use HTTPS between your client and the AtomicLoop server, and rotate test credentials after exercises.
PowerShell (Windows: powershell.exe) or PowerShell Core (Linux/macOS: pwsh) must be installed and on PATH. The executor is selected automatically based on the OS AtomicLoop is running on.
| Key | Default | Description |
|---|---|---|
port |
5011 |
HTTP port |
db_path |
./atomicloop.db |
SQLite database |
execution.timeout |
30 |
Default execution timeout (seconds) |
execution.require_confirm |
true |
Require explicit confirm flag |
execution.auto_save |
true |
Persist every run automatically |
integrations.lognorm_url |
http://127.0.0.1:5006 |
LogNorm endpoint |
integrations.driftwatch_url |
http://127.0.0.1:5008 |
DriftWatch endpoint |
| Variable | Required | Description |
|---|---|---|
ATOMICLOOP_API_KEY |
No | Shared secret required in the X-API-Key header on all POST /execute requests. If unset, the route is unauthenticated (suitable for local testing only). A warning is logged at startup when the variable is absent. |
# Example — set before starting the server
export ATOMICLOOP_API_KEY="change-me-before-exposing-to-a-network"
python app.pyWhen the key is configured, every POST /execute call must include the header:
X-API-Key: <your-key>
Requests with a missing or incorrect header receive 401 {"error": "unauthorized"}.
Key rotation:
ATOMICLOOP_API_KEYis read once at process startup (module import time). Changing the environment variable has no effect on a running server — the process must be restarted to pick up a new value.
AtomicLoop includes several controls to prevent accidental execution:
confirm: true— required in everyPOST /api/runbody to execute. Without it, the request is rejected.dry_run: true— shows the command without executing. Always safe. Supported on both/api/runand/execute.require_confirm: true(config) — server-enforced gate on all live executions.- Timeout — hard kill after N seconds (default 30). Applied to both local processes and WinRM sessions.
- Atomic allowlist —
/executeonly dispatches commands that appear verbatim in the embedded MITRE technique library. Arbitrary commands are rejected. ATOMICLOOP_API_KEY— when set,/executerequires a matchingX-API-Keyheader. All other routes are unaffected.cleanup_command— each test includes a cleanup command. Run it after testing.
Add to nebula-dashboard/config.yaml:
tools:
atomicloop:
label: "AtomicLoop"
url: "http://127.0.0.1:5011"
health_path: "/api/health"
description: "Atomic Red Team test runner and detection validator"
category: "Detection"This project is licensed under the MIT License — see the LICENSE file for details.
Built by Rootless-Ghost
Part of the Nebula Forge security tools suite.
