Detection rules › Kusto

Hunt for Defender for Identity NNR issues

Group by
DeviceName, IPAddress, NetworkAddrPrefix, RemoteIP, RemotePort
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This query can help you in finding Network Name Resolution health issues of Microsoft Defender for Identity. NNR is a critical component which is used to get more information on IP addresses seen by MDI. Without NNR proparly working, MDI can throw a lot of False Positive alerts.

References

Event coverage

Rule body yaml

let networks = DeviceNetworkInfo
    // Expand all IPs
    | mv-expand todynamic(IPAddresses)
    // Get the network address related to the IP
    | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(IPAddresses.SubnetPrefix))
    // Build the IP with the CIDR notation
    | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", tolong(IPAddresses.SubnetPrefix))
    // Save the Prefix as an extra property
    | extend Prefix = tostring(IPAddresses.SubnetPrefix)
    // Make a set of all the IP's belonging to the same subnet
    | summarize make_set(IPAddress) by NetworkAddress, Prefix
    // Count how many IPs there are in one subnet
    | extend CountIPs = array_length(set_IPAddress)
    | extend Joiner = 1;
// Network Information
let network_info = DeviceNetworkInfo
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress);
// Ports used in NNR
let nnr_ports = dynamic(["3389", "135", "137"]);
let mdi_servers = dynamic([]);
// Query network connections
DeviceNetworkEvents
// Get events from Defender for Identity sensors - fill in mdi-servers variable for more complete results
| where InitiatingProcessFileName == "Microsoft.Tri.Sensor.exe" or DeviceName has_any (mdi_servers)
// Check traffic for NNR ports
| where RemotePort in (nnr_ports)
// Join the network info for more destination context
| join kind=inner network_info on $left.RemoteIP == $right.IPAddress
// Get distinct values
| project-rename RemoteDeviceName = DeviceName1
| distinct DeviceName, ActionType, RemoteIP, RemotePort, RemoteDeviceName
// Join all network addresses
| extend Joiner = 1
| join kind=inner networks on Joiner
// Check if remote ip is in a certain network address
| extend NetworkAddrPrefix = strcat(NetworkAddress, "/", Prefix)
| where ipv4_is_in_range(RemoteIP, NetworkAddrPrefix)
// Create Object to reuse later
| extend Obj = pack(
    "DeviceName", DeviceName,
    "NetworkAddrPrefix", NetworkAddrPrefix,
    "RemotePort", RemotePort,
    "RemoteIP", RemoteIP
)
// Count amount of failed and succeeded logins
| summarize FailedConnections = countif(ActionType == "ConnectionFailed"), 
    SucceededConnections = countif(ActionType == "ConnectionSuccess") by tostring(Obj)
// Extract the columns from the object again
| extend Obj = todynamic(Obj)
// Save the properties for later use
| extend DeviceName = tostring(Obj.DeviceName),
    NetworkAddrPrefix = tostring(Obj.NetworkAddrPrefix),
    RemotePort = tostring(Obj.RemotePort),
    RemoteIP = tostring(Obj.RemoteIP)
// Create a new object to save the amount of failed and succeeded attempts per IP
| extend Obj = pack(
    "RemoteIP", RemoteIP,
    "SucceededConnections", SucceededConnections,
    "FailedConnections", FailedConnections
)
// Create a list of the remote ips and their connections by MDI sensor, destination subnet and RemoteIP
// Subnets with only fails on both ports will fail in NNR
| summarize ConnectionDetails = make_set(Obj), 
    TotalSucceededConnections = sum(SucceededConnections), 
    TotalFailedConnections = sum(FailedConnections) by DeviceName, NetworkAddrPrefix, RemotePort
// Filter out /32 addresses
| where NetworkAddrPrefix !contains "/32"
// Sorting
| sort by TotalFailedConnections desc
// Reorder
| project-reorder DeviceName, NetworkAddrPrefix, RemotePort, TotalSucceededConnections, TotalFailedConnections, ConnectionDetails

Stages and Predicates

Parameters

let nnr_ports = dynamic(["3389", "135", "137"]);
let mdi_servers = dynamic([]);

Let binding: networks

let networks = DeviceNetworkInfo
    | mv-expand todynamic(IPAddresses)
    | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(IPAddresses.SubnetPrefix))
    | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", tolong(IPAddresses.SubnetPrefix))
    | extend Prefix = tostring(IPAddresses.SubnetPrefix)
    | summarize make_set(IPAddress) by NetworkAddress, Prefix
    | extend CountIPs = array_length(set_IPAddress)
    | extend Joiner = 1;

Let binding: network_info

let network_info = DeviceNetworkInfo
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress);

Stage 1: source

DeviceNetworkEvents

Stage 2: where

| where InitiatingProcessFileName == "Microsoft.Tri.Sensor.exe" or DeviceName has_any (mdi_servers)

Stage 3: where

| where RemotePort in (nnr_ports)

Stage 4: join

| join kind=inner network_info on $left.RemoteIP == $right.IPAddress

Stage 5: project-rename

| project-rename RemoteDeviceName = DeviceName1

Stage 6: distinct

| distinct DeviceName, ActionType, RemoteIP, RemotePort, RemoteDeviceName

Stage 7: extend

| extend Joiner = 1

Stage 8: join

| join kind=inner networks on Joiner

Stage 9: extend

| extend NetworkAddrPrefix = strcat(NetworkAddress, "/", Prefix)

Stage 10: where

| where ipv4_is_in_range(RemoteIP, NetworkAddrPrefix)

Stage 11: extend

| extend Obj = pack(
    "DeviceName", DeviceName,
    "NetworkAddrPrefix", NetworkAddrPrefix,
    "RemotePort", RemotePort,
    "RemoteIP", RemoteIP
)

Stage 12: summarize

| summarize FailedConnections = countif(ActionType == "ConnectionFailed"), 
    SucceededConnections = countif(ActionType == "ConnectionSuccess") by tostring(Obj)

Stage 13: extend (3 consecutive steps)

| extend Obj = todynamic(Obj)
| extend DeviceName = tostring(Obj.DeviceName),
    NetworkAddrPrefix = tostring(Obj.NetworkAddrPrefix),
    RemotePort = tostring(Obj.RemotePort),
    RemoteIP = tostring(Obj.RemoteIP)
| extend Obj = pack(
    "RemoteIP", RemoteIP,
    "SucceededConnections", SucceededConnections,
    "FailedConnections", FailedConnections
)

Stage 14: summarize

| summarize ConnectionDetails = make_set(Obj), 
    TotalSucceededConnections = sum(SucceededConnections), 
    TotalFailedConnections = sum(FailedConnections) by DeviceName, NetworkAddrPrefix, RemotePort

Stage 15: where

| where NetworkAddrPrefix !contains "/32"

Stage 16: sort

| sort by TotalFailedConnections desc

Stage 17: project-reorder

| project-reorder DeviceName, NetworkAddrPrefix, RemotePort, TotalSucceededConnections, TotalFailedConnections, ConnectionDetails

Exclusions

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

FieldKindExcluded values
NetworkAddrPrefixcontains/32

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
DeviceNamematch
  • []
InitiatingProcessFileNameeq
  • Microsoft.Tri.Sensor.exe transforms: cased
RemotePortin
  • 135 transforms: cased corpus 5 (elastic 4, sigma 1)
  • 137 transforms: cased
  • 3389 transforms: cased corpus 11 (kusto 4, elastic 3, sigma 2, splunk 2)

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
ConnectionDetailssummarize
DeviceNamesummarize
NetworkAddrPrefixsummarize
RemotePortsummarize
TotalFailedConnectionssummarize
TotalSucceededConnectionssummarize