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 Access

References

Telemetry coverage

Rule body

// 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 base is inlined into the numbered stages below.

Stage 0: let

let base = DeviceInfo <inlined as stages below>;

Stages 1 to 4 define let base (the rule's main pipeline source); stages 5 to 11 run on it.

Stage 1: source

DeviceInfo

Stage 2: where

| where Timestamp > ago(30d)

Stage 3: extend

| extend AdditionalFields = todynamic(AdditionalFields)

Stage 4: extend

| 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 5: summarize

base
| summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol

Stage 6: join

| join kind=inner ( base()
    | summarize arg_min(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
) on DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol

Stage 7: extend

| extend Range = range(bin(InternetFacingLastSeen1, 1d), bin(InternetFacingLastSeen, 1d), 1d)

Stage 8: mv-expand

| mv-expand Range

Stage 9: where

| where Range != ""

Stage 10: summarize

| summarize count() by InternetFacingLocalPort, bin(todatetime(Range), 1d)

Stage 11: render

| render linechart

Output fields

These fields are emitted when the rule matches.

FieldSource
InternetFacingLocalPortsummarize