Detection rules › Kusto

Hunt Device Discovery Subnet Ranges

Group by
IPv4Dhcp, NetworkName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This KQL query helps you identify which subnet ranges are behind the Microsoft Defender for Endpoint Device Discovery 'Monitored Networks' page. By using this query you can investigate if all of your corporate networks are being monitored and change monitored states effectivly. More information can be found in the references.

References

Rule body yaml

// OPTIONAL - Device cap used to ignore network with less then X devices in them
let device_cap = 0;
DeviceNetworkInfo
| where Timestamp > ago(7d)
// Ignore empty networks
| where ConnectedNetworks  != ""
// Get networks data
| extend ConnectedNetworksExp = parse_json(ConnectedNetworks)
| mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp
| extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"])
// Get subnet data for IPv4 Addresses
| extend IPAddressesExp = parse_json(IPAddresses)
| mv-expand bagexpansion = array IPAddresses=IPAddressesExp
| extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"])
| extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix)
| extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix)
// Exclude IPv6 and APIPPA
| where SubnetPrefix <= 32
| where IPAddress !startswith "169.254"
// Ignore unidentified networks
| where not(NetworkName has_any ("Unidentified", "Identifying..."))
// Provide list
| distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange
| summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp
// Ignore network with very low device count
| where Devices >= device_cap
| sort by Devices desc

// OPTIONAL - Device cap used to ignore network with less then X devices in them
let device_cap = 0;
DeviceNetworkInfo
| where TimeGenerated > ago(7d)
// Ignore empty networks
| where ConnectedNetworks  != ""
// Get networks data
| extend ConnectedNetworksExp = parse_json(ConnectedNetworks)
| mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp
| extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"])
// Get subnet data for IPv4 Addresses
| extend IPAddressesExp = parse_json(IPAddresses)
| mv-expand bagexpansion = array IPAddresses=IPAddressesExp
| extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"])
| extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix)
| extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix)
// Exclude IPv6 and APIPPA
| where SubnetPrefix <= 32
| where IPAddress !startswith "169.254"
// Ignore unidentified networks
| where not(NetworkName has_any ("Unidentified", "Identifying..."))
// Provide list
| distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange
| summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp
// Ignore network with very low device count
| where Devices >= device_cap
| sort by Devices desc

Stages and Predicates

Parameters

let device_cap = 0;

Stage 1: source

DeviceNetworkInfo

Stage 2: where

| where Timestamp > ago(7d)

Stage 3: where

| where ConnectedNetworks  != ""

Stage 4: extend

| extend ConnectedNetworksExp = parse_json(ConnectedNetworks)

Stage 5: mv-expand

| mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp

Stage 6: extend

| extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"])

Stage 7: extend

| extend IPAddressesExp = parse_json(IPAddresses)

Stage 8: mv-expand

| mv-expand bagexpansion = array IPAddresses=IPAddressesExp

Stage 9: extend (3 consecutive steps)

| extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"])
| extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix)
| extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix)

Stage 10: where

| where SubnetPrefix <= 32

Stage 11: where

| where IPAddress !startswith "169.254"

Stage 12: where

| where not(NetworkName has_any ("Unidentified", "Identifying..."))

Stage 13: distinct

| distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange

Stage 14: summarize

| summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp
Threshold
ge 0

Stage 15: where

| where Devices >= device_cap

Stage 16: sort

| sort by Devices desc

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
IPAddressstarts_with169.254
NetworkNamematchUnidentified, Identifying...

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Devicesge
  • 0 transforms: cased
SubnetPrefixle
  • 32 transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Devicessummarize
IPv4Dhcpsummarize
NetworkNamesummarize
SubnetRangessummarize