A simplified simulation of an electrical grid outstation with DNP3 process control. Designed to serve as a target for Caldera for OT.
For an overview, read the writeup: Caldera for OT — Grid Watch: A Virtual DNP3 Electrical Grid Sandbox.
Created by University of Hawaii at Manoa students: Yueming Guo, Lewen Lin, Myra Angelica Ortigosa, and Justin Smith
In collaboration with Caldera for OT (ot@mitre.org).
This simulator provides a DNP3-enabled electrical grid outstation for testing and training. It includes a DNP3 outstation server and an HMI dashboard, useful for practicing protocol interactions without physical hardware.
- Python 3.11+
- dnp3py, pyyaml, matplotlib (see requirements.txt)
- Clone this repo:
git clone https://github.com/mitre/grid-watch.git
cd grid-watch- Install dependencies:
pip install -r requirements.txt- Run the simulator:
python run.pypython run.pySelect a component from the menu:
- 1 - Start the DNP3 outstation server (
0.0.0.0:20000) - 2 - Launch the matplotlib HMI dashboard (requires outstation running)
- 3 - Run the DNP3 test client
The outstation listens on TCP port 20000 and accepts connections from any standard DNP3 master. Caldera for OT with its DNP3 plugin is recommended for adversary emulation, but any DNP3 client (dnp3-actions, pyDNP3, etc.) will work.
The repo ships a fact source and four adversary profiles for use with Caldera for OT and its DNP3 plugin.
The files below are templates - copy them into your Caldera DNP3 plugin directory and adjust values (host, link addresses) for your environment:
| Template | Destination |
|---|---|
docs/sources/grid-simulator-facts.yml |
plugins/dnp3/data/sources/ |
docs/adversaries/dnp3-reconnaissance.yml |
plugins/dnp3/data/adversaries/ |
docs/adversaries/dnp3-unauthorized-control.yml |
plugins/dnp3/data/adversaries/ |
docs/adversaries/dnp3-grid-disruption.yml |
plugins/dnp3/data/adversaries/ |
docs/adversaries/dnp3-grid-attack-and-verify.yml |
plugins/dnp3/data/adversaries/ |
Restart Caldera after copying so it picks up the new files.
| Adversary | Scenario | Description |
|---|---|---|
| DNP3 Reconnaissance | Scenario 1 | Read-only enumeration of all outstation data points |
| DNP3 Unauthorized Control | Scenario 2 | Trip breaker and stop generator via DIRECT_OPERATE |
| DNP3 Grid Disruption | Scenario 3 | Multi-phase attack using DIRECT_OPERATE and SELECT_BEFORE_OPERATE |
| DNP3 Grid Attack and Verify | Scenario 4 | Full operate + read-back verification chain |
The docker/ directory contains a Docker Compose setup that brings up the outstation, a Caldera server with the DNP3 plugin, and a Sandcat agent on a shared network:
docker compose -f docker/docker-compose.yml up --build- Caldera UI: http://localhost:8888 (login:
admin/admin) - DNP3 Outstation:
localhost:20000
| Index | Name | Description |
|---|---|---|
| 0 | BusVoltage | Bus voltage (kV) |
| 1 | PowerDeficit | Power deficit (kW) |
| Index | Name | Description |
|---|---|---|
| 0 | BreakerStatus | Breaker state (true = closed) |
| 1 | GenStatus | Generator state (true = on) |
| Index | Name | Description |
|---|---|---|
| 0 | BreakerControl | Breaker state (true = closed) |
| 1 | GenControl | Generator state (true = on) |
pytest tests/ -vThe dnp3py library currently lacks support for qualifier 0x17 (indexed-count format),
which is the format this outstation uses for all object responses. This limitation was
tracked in craigpnnl/dnp3py issue #6.
To work around it, dnp3-sim/dnp3_frame.py handles link-layer framing and a custom
response parser was written in dnp3-sim/dnp3_read.py that handles the indexed-count
encoding directly.
- Caldera for OT - adversary emulation framework and DNP3 plugin this simulator is designed to target
- dnp3py - pure-Python DNP3 library used for the outstation and master implementations
- MITRE ATT&CK for ICS - threat model and tactic/technique taxonomy used in scenario design
Approved for public release. Distribution unlimited PRS 26-1239.

