Nzyme Alerts Go Mobile - Building a Wireless Bandit Pager

Nzyme Alerts Go Mobile - Building a Wireless Bandit Pager

When I first set up Nzyme and took it to BSides London, it was missing something pretty important: a way to alert me properly when chaos broke out.

At the time, the only supported alerting mechanism was email, and while that worked in theory, it didn’t quite fit the “real-time incident response” vibe I was going for.
So, I wrote a script to query the Nzyme database directly and used Pushover to send those alerts straight to my phone.

That setup worked brilliantly 😄 every time I powered up a rogue access point or triggered a Wi-Fi attack, my phone would light up and make a wonderfully dramatic klaxon sound.

Then came the SSID spam incident (oops). Hundreds of alerts later, my free Pushover quota was toast.
It was time to evolve and build something better.

The Idea: A Physical Alert Badge

Instead of relying on my phone I wanted a hardware alert device, something that would light up, beep, and generally make a fuss whenever Nzyme spotted a wireless bandit.

I’d tinkered with Badger 2040 and MagTag devices before, but this time I came across a brilliant post by Ayan Pahwa:

IoT with CircuitPython | Internet connected conference badge — Ayan Pahwa
The biggest selling point of CircuitPython to makers is its ease of usage. There is no driver or IDE installation required, just plug in the board, edit and save the “code.py” file inside the drive being mounted and you’re good to go, that’s how simple it is. This ease of usability and portabi

In his article, Ayan builds a cloud-connected badge using CircuitPython on the Adafruit PyBadge (a credit card sized device that can run CircuitPython or Arduino). It’s packed with fun features:

  • full-colour display
  • built-in speaker
  • Li-Po battery support
  • buttons and NeoPixels

Luckily the PyBadge also has co-processor support via FeatherWing, a pluggable board that brings extra capability, for example the Adafruit Airlift FeatherWing which has an ESP32 co-processor with Wi-Fi radio so we can connect to a wireless access point or hotspot.

This looked perfect for what I wanted - a portable device that can be battery powered, connects to Wi-Fi, has a colour screen and can make noises when an alert is triggered.

Add to this the simplicity of CircuitPython: just plug in the device and write code.

A Quick Win: Syslog Support

Right as I started planning, Nzyme added support for sending alerts to syslog which made life much easier. Instead of scraping the database, I could just tail syslog and process alerts however I wanted.

That became the backbone of my new alerting pipeline.

The Plan

Conceptually, I wanted this flow:

nzyme node → syslog → MQTT → badge → alert sound + display

Here’s how it fits together:

  1. Nzyme sends alerts to syslog
  2. A lightweight script tails syslog and publishes matching entries to MQTT
  3. The badge subscribes to the MQTT topic
  4. When an alert arrives, the badge lights up, plays a sound, and shows the alert on screen

Stage 1 - Nzyme Alert Configuration

We need to configure Nzyme to send alerts to Syslog. Nzyme has a nice 'events and actions' configuration, and it's simple enough to create a 'wildcard' alert that fires an alert on all detection types.

Example Nzyme alert configuration outputting to syslog.

I'm sending all of my alerts to the local syslog to keep things simple, but you could use a remote syslog service if you wanted 😄

Stage 2 - Syslog to MQTT Script

The script is running on the syslog host, in my case this is the same host as the Nzyme node. It watches syslog for alerts matching the 'Syslog Hostname' I configured in Nzyme (nzyme-alert), and then publishes the alert to my MQTT service.

I won't cover setting up MQTT, but there are Ubuntu packages that make it simple. You could even use a SaaS MQTT service (e.g. HiveMQ) if you wanted.

#!/bin/bash
LOG_FILE="/var/log/syslog"
PATTERN="nzyme-alert"

tail -F "$LOG_FILE" | grep --line-buffered "$PATTERN" | while read -r line
do
  ALERT_STRING=$(echo "${line}" | awk '{for(i=4;i<=NF;++i)printf $i" "; print ""}' | sed 's/"//g')
  /usr/bin/mqtt pub -i nzymealertscript -h mqtt.server.here -p 8883 \
  -u mqtt.username -pw 'mqtt.password' -r -t alerts -q 1 -m "${ALERT_STRING}" -d
done

Inelegant, but it works

I also created a systemd unit file to start this script on boot.

[Unit]
Description=nzyme-syslog-monitor
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
Restart=always
RestartSec=5
User=root
ExecStart=/opt/nzyme-syslog-monitor.sh

[Install]
WantedBy=multi-user.target

Stage 3 - Building the Badge

You can find the PyBadge for sale directly from Adafruit. At this time it's out of stock. You could instead get the EdgeBadge instead... same hardware layout, plus a few TensorFlow features I definitely don’t need (yet).

Adafruit PyBadge for MakeCode Arcade, CircuitPython, or Arduino
What&#39;s the size of a credit card and can run CircuitPython, MakeCode Arcade or Arduino? That&#39;s right, its the Adafruit PyBadge! We wanted to see how much we could cram into a ​3…

Since I'm using CircuitPython to build my application, I installed that on the device (see here) and downloaded Mu editor which is a nice, lightweight IDE with serial console support for the device.

As mentioned earlier, for Wi-Fi, I added the Adafruit Airlift FeatherWing ESP32 co-processor.

Adafruit AirLift FeatherWing – ESP32 WiFi Co-Processor
Give your Feather project a lift with the Adafruit AirLift FeatherWing - a FeatherWing that lets you use the powerful ESP32 as a WiFi co-processor. You probably have your favorite Feather…

A quick bit of soldering later and I had:

  • the badge with AirLift attached
  • a Li-Po battery
  • and an external speaker (because subtlety is overrated)

After tweaking the code for the latest CircuitPython libraries, I added graphics, icons, and alert sounds 😄 the fun bits.

Below is the code and a list of the libraries I've used from CircuitPython. Since it's python I didn't heavily comment the code, it's adaptable and genuinely I enjoyed writing it 😀

CircuitPython libraries I used in this project
import board
import busio
import neopixel
from os import getenv
from digitalio import DigitalInOut
from adafruit_pybadger import pybadger
import adafruit_minimqtt.adafruit_minimqtt as MQTT
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_connection_manager

sound = getenv("SOUND")
ssid = getenv("SSID")
ssid_pass = getenv("SSID_PASS")
ssid2 = getenv("SSID2")
ssid_pass2 = getenv("SSID_PASS2")
ssid3 = getenv("SSID3")
ssid_pass3 = getenv("SSID_PASS3")
mqtt_host = getenv("MQTT_HOST")
mqtt_port = getenv("MQTT_PORT")
mqtt_user = getenv("MQTT_USER")
mqtt_pass = getenv("MQTT_PASS")

# esp connection pins
esp32_cs = DigitalInOut(board.D13)
esp32_ready = DigitalInOut(board.D11)
esp32_reset = DigitalInOut(board.D12)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)

pool = adafruit_connection_manager.get_radio_socketpool(esp)
sslcontext = adafruit_connection_manager.get_radio_ssl_context(esp)

def wificonnect():
    while not esp.is_connected:
        try:
            if not esp.is_connected:
                esp.connect_AP(ssid, ssid_pass)
            if not esp.is_connected:
                esp.connect_AP(ssid2, ssid_pass2)
            if not esp.is_connected:
                esp.connect_AP(ssid3, ssid_pass3)
        except OSError as e:
            print("could not connect to AP, retrying: ", e)
            continue
#        print("Connected to", esp.ap_info.ssid, "\tRSSI:", esp.ap_info.rssi)

def connect(mqtt_client, userdata, flags, rc):
    print(f"Connected to MQTT Broker {mqtt_host}")
    mqtt_client.subscribe("alerts", 1)

def subscribe(mqtt_client, userdata, topic, granted_qos):
    print(f"Subscribed to {topic} with QOS level {granted_qos}")

def message(client, topic, message):
    if message == "":
        return

    print(f"New message on topic {topic}: {message}")

    if "with unexpected BSSID" in message:
        parts = message.split()
        network_name = parts[2]
        pybadger.show_badge(
            name_string=network_name, hello_string='Unexpected', my_name_is_string="BSSID for", hello_scale=2, my_name_is_scale=2, name_scale=2
        )
    elif "on unexpected frequency" in message:
        parts = message.split()
        network_name = parts[2]
        pybadger.show_badge(
            name_string=network_name, hello_string='Unexpected', my_name_is_string="frequency for", hello_scale=2, my_name_is_scale=2, name_scale=2
        )
    elif "Bandit" in message and "detected in range" in message and "Pwnagotchi" in message:
        parts = message.split()
        pybadger.show_badge(
            name_string=parts[4], hello_string="Bandit", my_name_is_string=parts[1], hello_scale=2, my_name_is_scale=2, name_scale=2
        )
    elif "Bandit" in message and "detected in range" in message and "WiFi Pineapple" in message:
        parts = message.split()
        pybadger.show_badge(
            name_string=parts[4], hello_string="Bandit", my_name_is_string="WiFi Pineapple", hello_scale=2, my_name_is_scale=2, name_scale=2
        )
    else:
        pybadger.show_badge(
            name_string=message, hello_scale=1, my_name_is_scale=1, name_scale=1
        )

    if sound != 0:
        pybadger.play_file(file_name="alert.wav")

    mqtt_client.publish("alerts", "", True, 1)

def mqconnect():
    mqtt_client.connect()

pybadger.show_business_card(image_name="nzymelogo.bmp")
if sound != 0:
    pybadger.play_file(file_name="startup.wav")

mqtt_client = MQTT.MQTT(
    client_id="badge",
    broker=mqtt_host,
    port=mqtt_port,
    username=mqtt_user,
    password=mqtt_pass,
    socket_pool=pool,
    is_ssl=False,
#    ssl_context=sslcontext,
    recv_timeout=11,
        connect_retries=3,
    socket_timeout=9,
)
mqtt_client.on_connect = connect
mqtt_client.on_subscribe = subscribe
mqtt_client.on_message = message

while True:
    try:
        pybadger.auto_dim_display()
        mqtt_client.loop(10)
    except Exception as e:
        try:
            print("Re-Connecting: ", e)
            wificonnect()
            mqconnect()
        except Exception as f:
            print("Re-Connecting Failed: ", f)
    print("polling....")

code.py

The Badge

Now whenever Nzyme detects a wireless network bandit, or a rogue access point, the badge lights up, plays a sound, and shows the alert in near real-time!

No phone apps, no cloud subscriptions, and no rate limits - just a self-contained little device that yells at me when the Wi-Fi gets spicy 🔥

And yes, it looks very cool handing off a lanyard at conferences.

Demo Time

0:00
/0:10

That's it for now... next up I want to explore some other Nzyme features including Bluetooth device identification (for all the Flipper Zero's out there) and UAV tracking.

I'll take Nzyme to BsidesLondon in December 2025, so if you want to know more just find me on the conference floor!