Detection rules › Kusto
Hunt for public facing devices and exposed ports over time
Find public facing devices over time via the public device tag in the DeviceInfo table.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1190 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.
| Field | Source |
|---|---|
InternetFacingLocalPort | summarize |