Every postmortem of a fast-moving outbreak ends with the same questions. Would segmentation have contained it? How much did the patch backlog matter? Would quarantining infected hosts at hour two have changed anything? In a real environment you cannot rerun the incident to find out. In a model, you can rerun it as many times as you like.
That is why I built the Worm Propagation Simulator. It is a small Python project that models how a self-replicating worm spreads across a network graph, and how defensive measures change the outcome. It is built for defensive analysis and teaching. It contains no exploit code, sends no network traffic and touches no real systems. Every “host” is a node in an in-memory graph, and every “infection” is a random number compared against a probability.
Repo: github.com/Willey2003/worm-propagation-simulator
How the model works
The simulation runs in ticks. The engine builds a network graph with NetworkX and gives every node a state: vulnerable, infected, patched or quarantined. Each node gets a security level of low, medium or high, which sets its chance of being compromised on a single attempt to 0.75, 0.5 or 0.25. A configurable share of nodes starts out already patched. One or more “patient zero” nodes start out infected.
On each tick, every infected node picks targets according to the chosen propagation model. For each target, the engine checks three things in order: whether the target is still susceptible, whether a firewall rule between their subnets blocks or rate-limits the attempt, and finally the probability roll. The engine records per-tick metrics as it goes: node counts by state, new infections, attempts, and attempts blocked by firewall rules.
What you can vary
| Dimension | Options in the code |
|---|---|
| Topology |
random (Erdős–Rényi), scale_free (Barabási–Albert), small_world (Watts–Strogatz), hierarchical (balanced tree), datacenter (fully meshed racks linked to each other) |
| Propagation model |
random_scan, hitlist, topological (graph neighbours only), permutation, local_preference (biased towards the node’s own subnet) |
| Baseline defence |
patch_rate: the fraction of nodes patched before the outbreak starts |
| Segmentation | Subnet-to-subnet firewall rules with deny or rate_limit (percentage of attempts allowed) |
| Mid-outbreak response | One scheduled intervention: patch_all, patch_subnet, quarantine_infected or rate_limit
|
| Reproducibility | A seed value fixes the random state, so runs are repeatable |
Nodes are assigned round-robin to subnets of about 20 nodes each. This is what gives the segmentation rules and the local-preference model something to act on.
The configs/ folder ships six YAML scenarios: five are loosely inspired by well-known worms (Code Red, SQL Slammer, Conficker, WannaCry, Mirai), plus a defensive_analysis scenario. Treat them as stylised parameter sets for comparison, not calibrated reproductions of history. A 500-node graph says nothing precise about the 2001 internet. What they do give you is a fair way to compare propagation behaviour and defensive choices against each other.
Running it
The core engine needs only NetworkX, NumPy and PyYAML. The test suite (17 tests covering nodes, topologies, propagation models, patching and firewall blocking) also runs in GitHub Actions on every push:
git clone https://github.com/Willey2003/worm-propagation-simulator.git
cd worm-propagation-simulator
pip install networkx numpy pyyaml pytest
python -m pytest tests/ -v
To run a scenario and print the summary:
import yaml
from src.simulator.engine import create_simulator_from_config
cfg = yaml.safe_load(open("configs/defensive_analysis.yaml"))
sim = create_simulator_from_config(cfg)
sim.run()
summary = sim.get_summary()
print({k: v for k, v in summary.items() if k != "infection_curve"})
The summary includes the peak infected count, the tick at which it peaked, total infections and attempts, and how many attempts were blocked. infection_curve holds the full per-tick history if you want to plot it.
The lesson that surprised me: timing beats thoroughness
The defensive_analysis scenario puts 500 nodes on a scale-free graph and 20% of them start patched. It applies two firewall rules between subnets and schedules a patch_all intervention at tick 30. With seed 42, that intervention never fires. The worm reaches every vulnerable node by tick 5 and the run ends, with 396 nodes infected.
Change one line, intervention_tick: 2, and the same patch_all caps the outbreak at 7 infected nodes out of 500.
The model is simple, but this is the same lesson real incidents keep teaching. Against a fast scanner, a complete response that arrives late is worth almost nothing. A response that arrives early is worth almost everything. That is the case for pre-approved containment playbooks and for patching exposed services before an outbreak, not during one.
Other entry points
The repo also includes:
- A Typer CLI with
run,compareandscenarioscommands, using Rich tables for output - A FastAPI service to create simulations, step them, apply interventions mid-run, fetch state over REST, and stream per-tick metrics over a WebSocket
- A Plotly visualisation module that writes the infection curve, network graph and a combined dashboard to HTML
- Docker, Docker Compose and Kubernetes manifests for running the API in a container
From the command line, the same runs are one command each:
python -m src.simulator.main scenarios
python -m src.simulator.main run --scenario code_red
python -m src.simulator.main compare --scenarios code_red,slammer
The compare command prints a side-by-side table of infections, peak timing and blocked attempts for each scenario.
Ideas for extending it
Ideas I would like to explore next: per-node detection delay before quarantine, more than one intervention per run, and calibrating scan rates against published outbreak data. If you teach network security, the topology comparison on its own makes a good lab exercise. Run the same worm on a random graph and on a scale-free graph and look at what the highly connected hubs do to the curve.
Get it
The source, scenario files and tests are at github.com/Willey2003/worm-propagation-simulator. Clone it, change one parameter at a time, and see which defences change the curve. If you build a scenario that teaches something useful, open a pull request.
Top comments (0)