Detection rules › Kusto

Hunt MDE with GSA events

Group by
DestinationFqdn, DestinationPort, InitiatingProcessFileName, InitiatingProcessName, Protocol, RemotePort, RemoteUrl, TransportProtocol
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This rule correlates the Microsoft Defender for Endpoint DeviceNetworkEvents table with the Global Secure Access NetworkAccessTraffic table. By doing this, you can enrich the MDE events which contains detailed process information with the GSA events that contains detailed HTTP header information and more.

References

Event coverage

Rule body yaml

let gsa_events = NetworkAccessTraffic
    // Join DeviceInfo to get MDE DeviceID
    | join kind=inner ( 
        DeviceInfo
        | distinct DeviceId, AadDeviceId
    ) on $left.DeviceId == $right.AadDeviceId
    // Remove Entra Device ID from GSA logs
    | project-away DeviceId
    // Rename MDE Device ID to DeviceId column
    | project-rename DeviceId = DeviceId1;
// Get all MDE network events
DeviceNetworkEvents
// Get HTTP details if HTTP connection is logged
| extend HttpStatus = toint(todynamic(AdditionalFields).status_code),
    BytesIn = toint(todynamic(AdditionalFields).response_body_len),
    BytesOut = toint(todynamic(AdditionalFields).request_body_len),
    HttpMethod = tostring(todynamic(AdditionalFields).method),
    UrlHostname = tostring(todynamic(AdditionalFields).host),
    UrlPath = tostring(todynamic(AdditionalFields).uri),
    UserAgent = tostring(todynamic(AdditionalFields).user_agent),
    HttpVersion = tostring(todynamic(AdditionalFields).version)
// Join GSA logs
| join kind=inner gsa_events on 
    DeviceId,
    $left.RemoteUrl == $right.DestinationFqdn,
    $left.RemotePort == $right.DestinationPort,
    $left.Protocol == $right.TransportProtocol,
    $left.InitiatingProcessFileName == $right.InitiatingProcessName
| project-rename TimeGeneratedGsa = TimeGenerated1, TimestampMde = Timestamp
| project-away Type, TenantId, TimeGenerated, TenantId1, Type1, DeviceId1, AadDeviceId

let gsa_events = NetworkAccessTraffic
    // Join DeviceInfo to get MDE DeviceID
    | join kind=inner ( 
        DeviceInfo
        | distinct DeviceId, AadDeviceId
    ) on $left.DeviceId == $right.AadDeviceId
    // Remove Entra Device ID from GSA logs
    | project-away DeviceId
    // Rename MDE Device ID to DeviceId column
    | project-rename DeviceId = DeviceId1;
// Get all MDE network events
DeviceNetworkEvents
// Get HTTP details if HTTP connection is logged
| extend HttpStatus = toint(todynamic(AdditionalFields).status_code),
    BytesIn = toint(todynamic(AdditionalFields).response_body_len),
    BytesOut = toint(todynamic(AdditionalFields).request_body_len),
    HttpMethod = tostring(todynamic(AdditionalFields).method),
    UrlHostname = tostring(todynamic(AdditionalFields).host),
    UrlPath = tostring(todynamic(AdditionalFields).uri),
    UserAgent = tostring(todynamic(AdditionalFields).user_agent),
    HttpVersion = tostring(todynamic(AdditionalFields).version)
// Join GSA logs
| join kind=inner gsa_events on 
    DeviceId,
    $left.RemoteUrl == $right.DestinationFqdn,
    $left.RemotePort == $right.DestinationPort,
    $left.Protocol == $right.TransportProtocol,
    $left.InitiatingProcessFileName == $right.InitiatingProcessName
| project-rename TimeGeneratedGsa = TimeGenerated2, TimestampMde = TimeGenerated
| project-away Type, TenantId, TimeGenerated, TenantId1, Type1, DeviceId1, AadDeviceId

Stages and Predicates

Let binding: gsa_events

let gsa_events = NetworkAccessTraffic
    | join kind=inner ( 
        DeviceInfo
        | distinct DeviceId, AadDeviceId
    ) on $left.DeviceId == $right.AadDeviceId
    | project-away DeviceId
    | project-rename DeviceId = DeviceId1;

Stage 1: source

DeviceNetworkEvents

Stage 2: extend

| extend HttpStatus = toint(todynamic(AdditionalFields).status_code),
    BytesIn = toint(todynamic(AdditionalFields).response_body_len),
    BytesOut = toint(todynamic(AdditionalFields).request_body_len),
    HttpMethod = tostring(todynamic(AdditionalFields).method),
    UrlHostname = tostring(todynamic(AdditionalFields).host),
    UrlPath = tostring(todynamic(AdditionalFields).uri),
    UserAgent = tostring(todynamic(AdditionalFields).user_agent),
    HttpVersion = tostring(todynamic(AdditionalFields).version)

Stage 3: join

| join kind=inner gsa_events on 
    DeviceId,
    $left.RemoteUrl == $right.DestinationFqdn,
    $left.RemotePort == $right.DestinationPort,
    $left.Protocol == $right.TransportProtocol,
    $left.InitiatingProcessFileName == $right.InitiatingProcessName

Stage 4: project-rename

| project-rename TimeGeneratedGsa = TimeGenerated1, TimestampMde = Timestamp

Stage 5: project-away

| project-away Type, TenantId, TimeGenerated, TenantId1, Type1, DeviceId1, AadDeviceId

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
BytesInextend
BytesOutextend
HttpMethodextend
HttpStatusextend
HttpVersionextend
UrlHostnameextend
UrlPathextend
UserAgentextend
TimeGeneratedGsaproject-rename
TimestampMdeproject-rename