Detection rules › Kusto
Hunt for devices organized by subnet
This rule helps you organize devices by subnet in your networks. By doing this, you can identify how many not-onboarded devices, devices not supporting MDE containment, and types of devices live in your subnet ranges.
References
Telemetry coverage
Rule body
let isolationSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2", "Linux", "macOS"]);
let containmentSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2"]);
let base = DeviceNetworkInfo
// Expand all IPs
| mv-expand todynamic(IPAddresses)
// Ignore IPv6 addresses
| where tostring(IPAddresses.IPAddress) !contains ":"
// Save the Prefix as an extra property and set it to /32 when empty
| extend Prefix = iff(isnotempty(tostring(IPAddresses.SubnetPrefix)), tostring(IPAddresses.SubnetPrefix), "32");
let networks = base
// Get network addresses with a non /32 prefix
| where Prefix != "32"
// Get the network address related to the IP
| extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix))
// Build the IP and Network Address with the CIDR notation
| extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", Prefix)
| extend NetworkAddress = strcat(NetworkAddress, "/", Prefix)
// Join the Device Info information
| join kind=inner DeviceInfo on DeviceId, ReportId
// Ignore APIPA addresses
| where NetworkAddress != "169.254.0.0/16"
// Ignore merged device IDs
| where MergedToDeviceId == ""
// Make a set of all the Device Objects belonging to the same subnet
| extend DeviceObj = pack(
"DeviceName", DeviceName,
"IPAddress", IPAddress,
"DeviceType", DeviceType,
"DeviceCategory", DeviceCategory,
"IsInternetFacing", IsInternetFacing,
"OnboardingStatus", OnboardingStatus,
"OSDistribution", OSDistribution,
"OSPlatform", OSPlatform
)
// Make a list of the objects in the same subnet
| summarize make_set(DeviceObj) by NetworkAddress;
let device_with_host_prefix = base
// Get network addresses with /32 Prefix to try and match other networks
| where Prefix == "32"
// Build the IP Address with the CIDR notation
| extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", Prefix)
// Join the Device Info information
| join kind=inner DeviceInfo on DeviceId, ReportId
// Ignore merged device IDs
| where MergedToDeviceId == ""
// Make a set of all the Device Objects
| extend DeviceObj = pack(
"DeviceName", DeviceName,
"IPAddress", IPAddress,
"DeviceType", DeviceType,
"DeviceCategory", DeviceCategory,
"IsInternetFacing", IsInternetFacing,
"OnboardingStatus", OnboardingStatus,
"OSDistribution", OSDistribution,
"OSPlatform", OSPlatform
)
| extend Joiner = 1;
let network_addresses = base
// Get network addresses with a non /32 prefix
| where Prefix != "32"
// Get the network address related to the IP
| extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix))
| extend NetworkAddress = strcat(NetworkAddress, "/", Prefix)
// Create joiner to find host addresses related to certain networks
| distinct NetworkAddress
| extend Joiner = 1;
let networks2 = device_with_host_prefix
// Try to join /32 IPs
| join kind=inner network_addresses on Joiner
// Check if IP is in the network range, and only return those IPs
| extend InRange = ipv4_is_in_range(IPAddress, NetworkAddress)
| where InRange == 1
// Make a list of the objects in the same subnet
| summarize make_set(DeviceObj) by NetworkAddress;
union networks, networks2
// Expand the Device Objects
| mv-expand set_DeviceObj
// Save the DeviceType, DeviceCategory, and Onboarding Status
| extend DeviceType = set_DeviceObj.DeviceType
| extend DeviceCategory = set_DeviceObj.DeviceCategory
| extend OnboardingStatus = set_DeviceObj.OnboardingStatus
// Count how many servers, workstations, network devices, iot devices, and ot devices exists in a subnet, the onboarding estate, and OS Distribution
| summarize Servers = countif(set_DeviceObj.DeviceType=="Server"),
Workstations = countif(set_DeviceObj.DeviceType=="Workstation"),
NetworkDevices = countif(set_DeviceObj.DeviceCategory=="NetworkDevice"),
IoTDevices = countif(set_DeviceObj.DeviceCategory=="IoT"),
OTDevices = countif(set_DeviceObj.DeviceCategory=="OT"),
Onboarded = countif(set_DeviceObj.OnboardingStatus=="Onboarded"),
NotOnboarded = countif(set_DeviceObj.OnboardingStatus!="Onboarded"),
IsolateSupportedOS = countif((set_DeviceObj.OSDistribution has_any (isolationSupportedOS) or set_DeviceObj.OSPlatform == "Linux") and set_DeviceObj.OnboardingStatus == "Onboarded"),
ContainSupportedOS = countif(set_DeviceObj.OSDistribution has_any (containmentSupportedOS) and set_DeviceObj.OnboardingStatus == "Onboarded") by NetworkAddress
// Join the network subnets so we have the device objects again
| join kind=leftouter networks on NetworkAddress
| join kind=leftouter networks2 on NetworkAddress
// Extend Array Concat
| extend set_DeviceObj = array_concat(set_DeviceObj, set_DeviceObj1)
// Remove duplicate columns
| project-away NetworkAddress1, NetworkAddress2, set_DeviceObj1
// Count how many IPs there are in one subnet
| extend CountIPs = array_length(set_DeviceObj)
| sort by CountIPs desc
Stages and Predicates
Stage 0: let
let isolationSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2", "Linux", "macOS"]);
let containmentSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2"]);
let base = DeviceNetworkInfo <inlined as stages below>;
let networks = base <inlined as stages below>;
let device_with_host_prefix = base <inlined as stages below>;
let network_addresses = base
| where Prefix != "32"
| extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix))
| extend NetworkAddress = strcat(NetworkAddress, "/", Prefix)
| distinct NetworkAddress
| extend Joiner = 1;
let networks2 = device_with_host_prefix <inlined as stages below>;
Stage 1: source
let base
Stage 2: source
let networks
Stage 3: source
let device_with_host_prefix
Stage 4: source
let network_addresses
Stage 5: source
let networks2
Stage 6: union
union of 2 branches
Stage 7: source
DeviceNetworkInfo
Stage 8: mv-expand
mv-expand
Stage 9: where
where not (IPAddress contains ":")
Stage 10: extend
extend Prefix
Prefix =if
/* macro: isnotempty(tostring(IPAddresses.SubnetPrefix)) */tostring(IPAddresses.SubnetPrefix)else
"32"Stage 11: where
where Prefix != 32
Stage 12: extend (3 consecutive steps)
extend IPAddress, NetworkAddress
Stage 13: join
join kind=inner (DeviceInfo) on DeviceId, ReportId
Stage 14: where
where NetworkAddress !~ "169.254.0.0/16"
Stage 15: where
where MergedToDeviceId =~ ""
Stage 16: extend
extend DeviceObj
Stage 17: summarize
summarize by NetworkAddress
Stage 18: source
DeviceNetworkInfo
Stage 19: mv-expand
mv-expand
Stage 20: where
where not (IPAddress contains ":")
Stage 21: extend
extend Prefix
Prefix =if
/* macro: isnotempty(tostring(IPAddresses.SubnetPrefix)) */tostring(IPAddresses.SubnetPrefix)else
"32"Stage 22: where
where Prefix == 32
Stage 23: extend
extend IPAddress
Stage 24: join
join kind=inner (DeviceInfo) on DeviceId, ReportId
Stage 25: where
where MergedToDeviceId =~ ""
Stage 26: extend
extend DeviceObj
Stage 27: extend
extend Joiner
Stage 28: join
join kind=inner (network_addresses) on Joiner
Stage 29: extend
extend InRange
Stage 30: where
where InRange == 1
Stage 31: summarize
summarize by NetworkAddress
Stage 32: mv-expand
mv-expand set_DeviceObj
Stage 33: extend (3 consecutive steps)
extend DeviceCategory, DeviceType, OnboardingStatus
Stage 34: summarize
summarize ContainSupportedOS, IoTDevices, IsolateSupportedOS, NetworkDevices, NotOnboarded, OTDevices, Onboarded, Servers, Workstations by NetworkAddress
Stage 35: join
join kind=leftouter (networks) on NetworkAddress
Stage 36: join
join kind=leftouter (networks2) on NetworkAddress
Stage 37: extend
extend set_DeviceObj
Stage 38: project-away
project-away NetworkAddress1, NetworkAddress2, set_DeviceObj1
Stage 39: extend
extend CountIPs
Stage 40: sort
sort by CountIPs
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
InRange | eq |
| field:"InRange" kind:eq value:"1" |
NetworkAddress | ne |
| field:"NetworkAddress" kind:ne value:"169.254.0.0/16" |
Prefix | eq |
| field:"Prefix" kind:eq value:"32" |
Prefix | ne |
| field:"Prefix" kind:ne value:"32" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
IPAddress | contains | : | excludes:IPAddress field:"IPAddress" value:":" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ContainSupportedOS | summarize |
IoTDevices | summarize |
IsolateSupportedOS | summarize |
NetworkAddress | summarize |
NetworkDevices | summarize |
NotOnboarded | summarize |
OTDevices | summarize |
Onboarded | summarize |
Servers | summarize |
Workstations | summarize |
set_DeviceObj | extend |
CountIPs | extend |