← Back to list

Hunting for USB anomalous devices

Yes, yes… I know. You have USB storage devices blocked by policy. Fine. But that does not mean you don’t have to worry about other devices…

jkb · 2026-07-16 22:28 · 0 claps · 4.0 min read
#cybersecurity #threat-hunting #kusto #kql #microsoft-defender-xdr
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval 🔒 · Cybersecurity

Hunting for USB anomalous devices

Yes, yes… I know. You have USB storage devices blocked by policy. Fine. But that does not mean you don’t have to worry about other devices. And even that policy — it probably has some exceptions… doesn’t it?

Either way, there are plenty examples of devices that should not be observed in corporate environment because they pose an obvious risk. For example digispark mouse or Teensy Keyboard which can be used for input injection etc. The question is — can you find them? Where do you even look for it in the data?

Well, if you are using Microsoft Defender XDR — I have good news for you. There is plenty of data and DeviceEvents table does not disappoint yet again. If you search for ActionType PnpDeviceConnected you will find a lot of events, and I mean a lot.

You can narrow it down to just USB devices like this

DeviceEvents
| where TimeGenerated > ago(3d)
| where ActionType == "PnpDeviceConnected"
| extend AdditionalFields = todynamic(AdditionalFields)
| extend USBDeviceId = AdditionalFields.DeviceId, 
    USBDeviceClassName = AdditionalFields.ClassName, 
    USBDeviceDescription = AdditionalFields.DeviceDescription
| project USBDeviceId, USBDeviceDescription, USBDeviceClassName
| where USBDeviceClassName startswith "USB"

What you will see:

PnpDeviceConnected results with USB devices

PnpDeviceConnected results with USB devices

Device description is sometimes useful but sometimes it states something generic like USB Mass Storage Device or USB Composite Device.

It left me wanting more, so I turned towards that oddly looking DeviceId.

(By the way, try using bag_unpack on AdditionalFields and see weird column name collision for DeviceId ;) )

DeviceId decomposition

Lets split the value USB\VID_1462&PID_3FA4&MI_01\8&31b27b46&0&0001 first on backslashes:

  1. USB — Bus type

  2. 8&31b27b46&0&0001 — Instance ID so system can reference specific device

  3. VID_1462&PID_3FA4&MI_01 — this one we split on & and we get

3.1. VID_1462 — Vendor ID

3.2. PID_3FA4 — Product ID

3.3 MI_01 — if device exposes multiple interfaces (composite device) this is the interface ID.

We can of course do this in KQL as well:

DeviceEvents
| where TimeGenerated > ago(3d)
| where ActionType == "PnpDeviceConnected"
| extend AdditionalFields = todynamic(AdditionalFields)
| extend USBDeviceId = AdditionalFields.DeviceId, 
    USBDeviceClassName = AdditionalFields.ClassName, 
    USBDeviceDescription = AdditionalFields.DeviceDescription
| project USBDeviceId, USBDeviceDescription, USBDeviceClassName
| where USBDeviceClassName startswith "USB"
| extend DevArray = split(USBDeviceId, "\\")
| extend BUS_TYPE = DevArray[0], VENDOR_PRODUCT = DevArray[1], INSTANCE = DevArray[2]
| extend VP_ARRAY = split(VENDOR_PRODUCT, "&")
| extend VENDOR = VP_ARRAY[0], PRODUCT = VP_ARRAY[1], INTERFACE_ID = VP_ARRAY[2], COLLECTION = VP_ARRAY[3]
| where VENDOR startswith "VID_" and PRODUCT startswith "PID_"
| project-reorder VENDOR, PRODUCT, INTERFACE_ID, COLLECTION

Vendor and Product ID parsed in KQL

Vendor and Product ID parsed in KQL

Now, the only thing we need is some knowledge source that would allow us to translate those IDs to actual vendor names and product names. Luckily, amazingly(!), there is such source. Its user-maintained not centrally managed but surprisingly accurate and up to date (last update 06.2026). You can find it here: http://www.linux-usb.org/usb.ids.

A simple couple of lines in python to parse it to our needs:

import json
import pandas
from requests import get

usb_url = "http://www.linux-usb.org/usb.ids"

def parse_vendor(line):
    line_array = line.split('  ')
    vendor_id = line_array[0]
    vendor_name = line_array[1]
    return (vendor_id, vendor_name)

def parse_device(line):
    line_array = line.strip("\t").split('  ')
    device_id = line_array[0]
    device_name = line_array[1]
    return (device_id, device_name)

response = get(usb_url)
if response.ok:
    data = response.text

data = data.split("\n")

vendor_id = None
vendor_name = None
records = []

records = []
for line in data:
    line = line.strip("\n")

    if line:
        if "# List of known device classes, subclasses and protocols" in line: # end of usb vendor / product data
            break
        else:
            if line[0] != "#": # skip comments
                if line[0] == "\t":
                    device_id, device_name = parse_device(line)
                    records.append({
                        "Vendor_ID": vendor_id,
                        "Vendor_Name": vendor_name,
                        "Product_ID": device_id,
                        "Product_Name": device_name
                    })
                else:
                    vendor_id, vendor_name = parse_vendor(line)

df = pandas.DataFrame.from_records(records)
df.to_csv('usb2.csv', quoting=1, quotechar="\"", index=False)

And we get a nice CSV file. Here is mine: https://raw.githubusercontent.com/jkb-s/th/refs/heads/main/usb.csv

"Vendor_ID","Vendor_Name","Product_ID","Product_Name"
"0001","Fry's Electronics","7778","Counterfeit flash drive [Kingston]"
"0002","Ingram","0002","passport00"
"0002","Ingram","7007","HPRT XT300"

We can of course reference this file through kusto function externaldata:

let USB_ID = externaldata(Vendor_ID:string, Vendor_Name: string, Product_ID: string, Product_Name:string)
[
    "https://raw.githubusercontent.com/jkb-s/th/refs/heads/main/usb.csv"
]
with (format="csv", ignoreFirstRecord=true);
USB_ID

Finally, we combine it with our data, after trimming down Vendor/Product IDs and unifying the letter capitalization (CSV file has lowercase) to make sure the table join will work, and voila:

let USB_ID = externaldata(Vendor_ID:string, Vendor_Name: string, Product_ID: string, Product_Name:string)
[
    "https://raw.githubusercontent.com/jkb-s/th/refs/heads/main/usb.csv"
]
with (format="csv", ignoreFirstRecord=true);
DeviceEvents
| where TimeGenerated > ago(1d)
| where ActionType == "PnpDeviceConnected"
| extend AdditionalFields = todynamic(AdditionalFields)
| extend USBDeviceId = AdditionalFields.DeviceId, USBDeviceClassName = AdditionalFields.ClassName, USBDeviceDescription = AdditionalFields.DeviceDescription
| extend DevArray = split(USBDeviceId, "\\")
| extend BUS_TYPE = DevArray[0], VENDOR_PRODUCT = DevArray[1], INSTANCE = DevArray[2]
| extend VP_ARRAY = split(VENDOR_PRODUCT, "&")
| extend VENDOR = VP_ARRAY[0], PRODUCT = VP_ARRAY[1], INTERFACE_ID = VP_ARRAY[2], COLLECTION = VP_ARRAY[3]
| where VENDOR startswith "VID_" and PRODUCT startswith "PID_"
| extend Vendor_ID = tolower(tostring(split(VENDOR, "_")[1]))
| extend Product_ID = tolower(tostring(split(PRODUCT, "_")[1]))
| project Vendor_ID, Product_ID, AdditionalFields, DeviceId, DeviceName
| join kind=inner (USB_ID) on Vendor_ID, Product_ID
| summarize dcount(DeviceId) by Vendor_Name, Product_Name

Whats next?

You can search in results for specific type of device that I mentioned above and check if they are present in your environment and of course investigate if they should be. You can check for rare devices or export results from your query and ask some “neat AI” to check for Vendor/Product that require the most attention from cybersecurity or compliance perspective.

I hope you liked it and found it useful. Maybe even some true positive incident was found this way… that would be awesome. Let me know :)


메타데이터
post_id
6007c8b70eb4
slug
hunting-for-usb-anomalous-devices-6007c8b70eb4
url
https://medium.com/@jszu_31138/hunting-for-usb-anomalous-devices-6007c8b70eb4
canonical_url
https://medium.com/@jszu_31138/hunting-for-usb-anomalous-devices-6007c8b70eb4
author_url
https://medium.com/@jszu_31138
status
ok
fetched_at
2026-08-12 12:54:48