Detection rules › Kusto

Hunt for public facing devices and exposed ports over time

Group by
DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

Find public facing devices over time via the public device tag in the DeviceInfo table.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1190 Exploit Public-Facing Application

References

Rule body yaml

// Create a base function
let base = (){ 
    DeviceInfo
    | where Timestamp > ago(30d)
    | extend AdditionalFields = todynamic(AdditionalFields)
    | extend InternetFacingLastSeen = todatetime(AdditionalFields.InternetFacingLastSeen)
        , InternetFacingReason = tostring(AdditionalFields.InternetFacingReason)
        , InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp)
        , InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp)
        , InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort)
        , InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort)
        , InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol)
};
base()
// Get the latest resport
| summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
// Join with the earliest report
| join kind=inner ( base()
    | summarize arg_min(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
) on DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
// Make a data point for each day between earliest and latest report
| extend Range = range(bin(InternetFacingLastSeen1, 1d), bin(InternetFacingLastSeen, 1d), 1d)
// Now expand all datapoints for dates the ports have been active
| mv-expand Range
| where Range != ""
| summarize count() by InternetFacingLocalPort, bin(todatetime(Range), 1d)
| render linechart

Stages and Predicates

Let binding: base

let base = (){ 
    DeviceInfo
    | where Timestamp > ago(30d)
    | extend AdditionalFields = todynamic(AdditionalFields)
    | extend InternetFacingLastSeen = todatetime(AdditionalFields.InternetFacingLastSeen)
        , InternetFacingReason = tostring(AdditionalFields.InternetFacingReason)
        , InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp)
        , InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp)
        , InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort)
        , InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort)
        , InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol)
};

Stage 1: source

DeviceInfo

Stage 2: where

where ...

Stage 3: extend

extend AdditionalFields

Stage 4: extend

extend InternetFacingLastSeen, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingReason, InternetFacingTransportProtocol

Stage 5: summarize

summarize by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol

Stage 6: join

join kind=inner (...)

Stage 7: extend

extend Range

Stage 8: mv-expand

mv-expand Range

Stage 9: where

where Range !~ ""

Stage 10: summarize

summarize by InternetFacingLocalPort

Stage 11: render

render

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
InternetFacingLocalPortsummarize