Removable media is one of those threat vectors that gets underestimated. A USB drive sitting on a desk looks harmless, but it might contain a live Linux environment, a Windows installer, a forensic toolkit, or a persistence mechanism that bypasses the running OS entirely. Standard endpoint controls don’t help much if someone boots from USB before your agent ever starts.

This writeup covers a Wazuh detection use case for identifying bootable USB media on Linux endpoints. The detection is heuristic, meaning it combines filesystem type, volume label patterns, and known boot artifact paths to make an informed judgment about whether a connected USB device is likely bootable. It won’t catch everything, but it gives you visibility you wouldn’t otherwise have.

One important constraint to understand upfront: Wazuh is a host-based tool. It only operates once the monitored OS and agent are running. That means this use case cannot stop a BIOS or UEFI boot decision, and it cannot prevent someone from booting into another OS before the agent loads. What it can do is detect the presence of bootable media while the monitored system is running and generate an alert for SOC review.

The detection approach

Rather than relying on a single indicator, the detection script layers three types of checks.

Filesystem type. iso9660 and udf are almost exclusively used by ISO-based boot media. vfat and exfat are common on EFI boot partitions. ntfs shows up on Windows installers.

Volume labels. Labels like UEFI, TAILS, VENTOY, KALI, CCCOMA_ (the Windows installer label), and similar strings are strong signals that a partition contains installer or live media.

Boot artifact paths. If the first two checks don’t catch it, the script mounts the partition read-only and looks for known bootloader files: EFI binaries, GRUB configs, Windows boot manager files, isolinux, ldlinux.sys, live/filesystem.squashfs, and others.

This layered approach gives reasonable coverage across Windows installers, Linux live media, and common rescue tools without depending on any single indicator that could be spoofed or absent.

The detection script

Place this at /var/ossec/active-response/bin/check_usb_bootable.sh on the monitored Linux endpoint:

#!/bin/bash

TMPDIR="/tmp/wazuh-usb-check"
mkdir -p "$TMPDIR"

found=0

get_pair_value() {
  local line="$1"
  local key="$2"
  printf '%s\n' "$line" | sed -n "s/.*${key}=\"\\([^\"]*\\)\".*/\\1/p"
}

add_csv_unique() {
  local current="$1"
  local item="$2"
  [ -z "$item" ] && { printf '%s\n' "$current"; return; }
  case ",$current," in
    *,"$item",*) printf '%s\n' "$current" ;;
    *)
      if [ -n "$current" ]; then printf '%s,%s\n' "$current" "$item"
      else printf '%s\n' "$item"; fi ;;
  esac
}

for dev in $(lsblk -dn -o NAME,TRAN,TYPE | awk '$2=="usb" && $3=="disk" {print $1}'); do
  device_detected=0
  reasons=""
  labels=""

  while IFS= read -r line; do
    NAME="$(get_pair_value "$line" "NAME")"
    FSTYPE="$(get_pair_value "$line" "FSTYPE")"
    LABEL="$(get_pair_value "$line" "LABEL")"
    MOUNTPOINT="$(get_pair_value "$line" "MOUNTPOINT")"

    [ -z "$NAME" ] && continue
    [ "$NAME" = "$dev" ] && continue

    part="/dev/$NAME"
    bootable=0
    reason=""
    mnt="$MOUNTPOINT"
    mounted_here=0

    case "$FSTYPE" in
      iso9660|udf)
        bootable=1; reason="image_fs" ;;
      vfat|fat|exfat)
        if printf '%s\n' "$LABEL" | grep -Eiq \
          'TAILS|UEFI|EFI|VENTOY|LIVE|INSTALL|BOOT|CCCOMA_|ROCKY|UBUNTU|DEBIAN|KALI|FEDORA|RHEL|CENTOS|MINT|ARCH|MANJARO'; then
          bootable=1; reason="common_boot_label_matched"
        fi ;;
      ntfs)
        if printf '%s\n' "$LABEL" | grep -Eiq 'CCCOMA_|WIN|BOOT|INSTALL'; then
          bootable=1; reason="windows_boot_label_matched"
        fi ;;
    esac

    if [ -z "$mnt" ]; then
      mnt="$TMPDIR/$NAME"
      mkdir -p "$mnt"
      if mount -o ro "$part" "$mnt" 2>/dev/null; then mounted_here=1
      else mnt=""; fi
    fi

    if [ "$bootable" -eq 0 ] && [ -n "$mnt" ] && [ -d "$mnt" ]; then
      if [ -f "$mnt/EFI/BOOT/BOOTX64.EFI" ] || [ -f "$mnt/EFI/BOOT/BOOTIA32.EFI" ] || \
         [ -f "$mnt/EFI/BOOT/GRUBX64.EFI" ] || [ -f "$mnt/efi/boot/bootx64.efi" ] || \
         [ -f "$mnt/bootmgr" ] || [ -f "$mnt/bootmgr.efi" ] || \
         [ -f "$mnt/sources/boot.wim" ] || [ -f "$mnt/sources/install.wim" ] || \
         [ -f "$mnt/isolinux/isolinux.bin" ] || [ -f "$mnt/syslinux/syslinux.cfg" ] || \
         [ -f "$mnt/ldlinux.sys" ] || [ -f "$mnt/boot/grub/grub.cfg" ] || \
         [ -f "$mnt/boot/grub2/grub.cfg" ] || [ -f "$mnt/live/filesystem.squashfs" ] || \
         [ -d "$mnt/live" ] || [ -d "$mnt/casper" ] || [ -f "$mnt/.disk/info" ]; then
        bootable=1; reason="boot_heuristics_matched"
      fi
    fi

    [ "$mounted_here" -eq 1 ] && umount "$mnt" 2>/dev/null

    if [ "$bootable" -eq 1 ]; then
      device_detected=1
      reasons="$(add_csv_unique "$reasons" "$reason")"
      labels="$(add_csv_unique "$labels" "$LABEL")"
    fi
  done < <(lsblk -P -n -o NAME,FSTYPE,LABEL,MOUNTPOINT "/dev/$dev")

  if [ "$device_detected" -eq 1 ]; then
    echo "USB_BOOTABLE_MEDIA_DETECTED device=/dev/$dev reasons=${reasons:-unknown} labels=${labels:-none}"
    found=1
  fi
done

[ "$found" -eq 0 ] && echo "USB_BOOTABLE_MEDIA_NOT_DETECTED"
exit 0

Set correct permissions:

chmod 750 /var/ossec/active-response/bin/check_usb_bootable.sh
chown root:wazuh /var/ossec/active-response/bin/check_usb_bootable.sh

The script uses lsblk to find USB disks by transport type, then walks through their partitions one by one. For each partition it checks filesystem type and label first since those are fast and require no mounting. If those checks are inconclusive, it mounts the partition read-only into a temp directory and inspects the file structure. Everything gets cleaned up after inspection.

The output is intentionally simple, one of two strings:

USB_BOOTABLE_MEDIA_DETECTED device=/dev/sda reasons=common_boot_label_matched labels=KALI
USB_BOOTABLE_MEDIA_NOT_DETECTED

Keeping the output this clean makes the Wazuh rule matching easy to write and easy to read later.

Agent configuration

Add this block inside <ossec_config> in /var/ossec/etc/ossec.conf on the agent:

<wodle name="command">
  <disabled>no</disabled>
  <tag>usb_bootable_check</tag>
  <command>/var/ossec/active-response/bin/check_usb_bootable.sh</command>
  <interval>5s</interval>
  <ignore_output>no</ignore_output>
  <run_on_start>yes</run_on_start>
  <timeout>10</timeout>
  <skip_verification>yes</skip_verification>
</wodle>

The command wodle runs the script on a scheduled interval and forwards the output to the manager for rule matching. Five seconds is fast enough to catch something without hammering the system. The 10 second timeout prevents a hung mount from blocking the agent.

Restart the agent after saving:

sudo /var/ossec/bin/wazuh-control restart

Detection rules

On the Wazuh manager, add these to /var/ossec/etc/rules/local_rules.xml:

<group name="usb_boot_media,custom,linux,">

  <rule id="100251" level="8">
    <decoded_as>command</decoded_as>
    <match>USB_BOOTABLE_MEDIA_DETECTED</match>
    <description>Bootable USB media detected on Linux endpoint</description>
    <group>usb_boot_media,policy_violation,linux,</group>
  </rule>

  <rule id="100252" level="0">
    <decoded_as>command</decoded_as>
    <match>USB_BOOTABLE_MEDIA_NOT_DETECTED</match>
    <options>no_log</options>
  </rule>

</group>

Rule 100251 fires at level 8 when bootable media is found. Rule 100252 handles the expected negative output and suppresses it completely with no_log. Without that second rule, every clean poll generates a log entry and the noise adds up fast.

Restart the manager:

/var/ossec/bin/wazuh-control restart

Validating the pipeline

Test the script directly on the agent first before depending on Wazuh automation:

sudo /var/ossec/active-response/bin/check_usb_bootable.sh

Then watch the manager alert log in real time:

tail -f /var/ossec/logs/alerts/alerts.json

Testing the script manually first is worth the extra step. If something is wrong with the detection logic you want to find out before you are wondering why alerts aren’t coming through. Validating script, agent, rule, and alert separately makes troubleshooting a lot less painful.

A note on active response

Wazuh supports an active response that unmounts USB partitions and removes the device from the kernel device tree via /sys/block/$dev/device/delete. It works, but in most environments automatic containment should be treated as an advanced option rather than the default.

The detection is heuristic. Some USB devices may be legitimately needed by the user. And Wazuh can only act after the device has already been visible to the OS, so containment is always reactive. Triggering an automatic response based on a label match and accidentally cutting off a legitimate device is a real possibility.

In practice this kind of use case works best as detection with SOC review first, with containment reserved for high-risk assets or repeated detections from the same host.

What this catches and what it doesn’t

This will reliably detect common Linux live media like Kali, Ubuntu, Tails, Fedora, and Arch, along with Windows installer media, Ventoy multi-boot drives, and most EFI-bootable recovery tools.

It won’t catch custom or unlabeled boot media that avoids known filesystem patterns, devices that were connected and removed before the agent polled, or anything that happened before the monitored OS loaded.

That last point is worth internalizing. Host-based detection has a hard floor at OS boot time. For complete coverage, USB boot controls need to be enforced at the firmware level with BIOS/UEFI boot order restrictions and a firmware password. This Wazuh detection is a useful layer on top of that, but it’s not a replacement for it.