MSc THESIS · UvA SNE × KPMG CYBER

The clean-up step that
broke the classifier

An obvious fix that quietly erases the evidence.

Stable-Edge Filtering for Passive OT Device Classification under Operational Change
Jonathan van den Heuvel · jvdh.tech

THE PROBLEM

We classify what we can't scan

  • Passive OT asset discovery is a routine KPMG deliverable.
  • You can't active-scan a live plant, so we classify devices from captured traffic.
  • But real segments are messy: laptops connect, scanners sweep, maintenance happens.

Does a simple clean-up step keep the classifier robust under that change?

Passive capture on a shared segment TAP benchmarks: clean 1-hour captures client traffic: never is

THE IDEA + THE LAB

Keep only the connections that last

  • Build the graph of who-talks-to-whom, then drop edges that don't persist over time.
  • Intuition: a one-off engineering session or a scan sweep is noise, filter it out.
Stable-edge filtering prunes transient edges keep drop

THE TESTBED

20
HOSTS
5
CLASSES · ctrl · sup · eng · hist · it
4
CHANGE SCENARIOS
10×10
LAB × MODEL SEEDS
INDUCTIVE (held-out hosts) · GraphSAGE

builds on Heo & Shin (2025) · artifacts released

BUILDING THE LAB · THE FACTORY

What the devices actually do

What the devices do — the bottling factory FACTORY FLOOR bottles flow left → right · one controller drives each stage Storage Tank holds product before fill controller · plc-1 Filling Station fills empty bottles controller · plc-2 Capping Line seals each bottle controller · plc-3 Labeling Line applies labels & QR codes controller · plc-4 CONTROL ROOM operators watch the line hmi-1 supervisory station hmi-2 supervisory station hmi-3 supervisory station hmi-4 supervisory station ENGINEERING configure & diagnose controllers eng-1 engineering workstation eng-2 engineering workstation eng-3 engineering workstation eng-4 engineering workstation SERVER ROOM record every PLC reading historian-1 historian historian-2 historian historian-3 historian historian-4 historian IT / OT BOUNDARY gateways bridge to enterprise it-gw-1 it-gw-2 it-gw-3 it-gw-4
real roles → real traffic → the who-talks-to-whom graph the classifier reads

BUILDING THE LAB · THE TESTBED

20 containers, one passive tap

  • A small plant in Docker: 5 device classes, 4 each, on one shared segment.
  • Every container runs the same image, and its class decides whether it serves or polls.
# one image; class set in lab.yaml
dev = DEVICE_CLASSES[host.class]
dev.setup()        # PLC: serves :502
while running:
    phase = read("phase.json")
    for plc in dev.targets:  # client
        poll(plc)   # HMI · hist · eng
    sleep(dev.poll_interval)

RUNS 24/7 on a server · 78 scenario runs · 24 GB captured

The testbed: five device classes on one tapped segment PLC ×4 serves HMI ×4 poll 1 s ENG ×4 poll 5–10 s HIST ×4 poll 2 s IT ×4 DNS/NTP 30 s one shared OT segment · 172.25.0.0/24 TAP merged pcap Zeek flow graph one passive tap → the who-talks-to-whom graph the model sees

BUILDING THE LAB · OPERATIONAL CHANGE

Four scripted changes,
known ground truth

  • A phase signal tells every container when the world changes, live, mid-capture.
  • So I can perturb the network and still know the truth.
for phase in scenario.phases:
    write("phase.json", phase.effects)
    sleep(phase.minutes) # live
labels = label_edges(config)
# truth from CONFIG, not the traffic
# "stable" iff seen in EVERY phase

FIVE STATES · FOUR CHANGES

steady
nothing changes · the baseline
maintenance
plc-1 paused 40 min, its edges vanish→ this is the one that breaks
onboarding
a new HMI joins mid-run
drift
an engineer repoints to another PLC
noise
a scanner sweeps the segment

edge labels come from config, not traffic, no circularity

RQ2 · THE RESULT (held-out macro-F1)

Neutral on four.
It breaks the fifth.

  • Neutral on 4 of 5 scenarios: steady, onboarding, config drift, benign scanning.
  • No robustness benefit anywhere, the upside we hoped for simply isn't there.
  • Under maintenance it significantly hurts.
0.45 0.36 −0.089 · p = 0.027 · 8/10 runs worse

THE MECHANISM · MAINTENANCE

A paused controller looks like an idle laptop

  1. 1A controller (PLC) is paused for 40 minutes.
  2. 2Its polls stop, the “keep persistent edges” filter deletes all 20. in-degree 20→0 · in-bytes 2.1M→0
  3. 3Stripped bare, it has zero traffic, identical to an idle IT laptop → misread as “IT endpoint.”

A plain random forest breaks the same way, the filter destroys the evidence, not the model.

THE TAKEAWAY

Persistence is the wrong thing to filter on.

A cheap clean-up step can quietly erase the evidence your classifier depends on.

  • Don't pre-filter passive OT graphs by recurrence.
  • Edge meaning beats persistence, think protocol, direction, endpoint roles.

Reproducible lab · 4 scenarios · code released  |  jvdh.tech · Jonathan van den Heuvel · github.com/jonathanvdheuvel

    Stable-Edge Filtering · KPMG Cyber Tech Resilience 2 / 9

    KPMG CYBER · TECH RESILIENCE

    Which connections can you safely throw away?

    Stable-Edge Filtering for Passive OT Device Classification
    Jonathan van den Heuvel · jvdh.tech

    → to begin · ? for help