Detection rules › Kusto

Suspicious Network Beacons - Microsoft Defender for Endpoint Aggregated Reports

Group by
DeviceName, InitiatingProcessFileName, RemoteIP
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query detects suspicious beaconing activity by analyzing DeviceNetworkEvents Aggregated Reports telemetry. Use it as a starting point and refine further as it may generate too many results.

MITRE ATT&CK coverage

TacticTechniques
Command & ControlNo specific technique

Event coverage

Rule body kusto

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://academy.bluraven.io/blog/beaconing-detection-using-mde-aggregated-report-telemetry
//
// Query parameters:
let lookback = 3d;
let min_uniform_count = 4 * (lookback / 1d); // (4 uniform distribution per lookback)
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where ActionType endswith "AggregatedReport"
| where ipv4_is_private(RemoteIP) == false
| extend ConnectionCount = toint(parse_json(AdditionalFields).uniqueEventsAggregated)
| project Timestamp = bin(Timestamp, 1h), DeviceName, InitiatingProcessFileName, RemoteIP, ConnectionCount
| sort by Timestamp asc 
| summarize Timestamp = make_list(Timestamp), ConnCounts = make_list(ConnectionCount) by DeviceName, InitiatingProcessFileName, RemoteIP
| extend count_of_hours = array_length(Timestamp)
| extend anomalies_decomposed = series_decompose_anomalies(ConnCounts, 1.5, -1),
        series_stats(ConnCounts)
| mv-apply anomaly = anomalies_decomposed to typeof(int) on (
                summarize inliner_count = countif(anomaly !in (-1, 1)), outlier_count = countif(anomaly in (-1, 1))
    )
| where inliner_count >= min_uniform_count and series_stats_ConnCounts_avg > 45 // avg=45 is roughly 1 min sleep with some jitter
| sort by series_stats_ConnCounts_avg desc 
| extend FirstConnection = Timestamp[0], LastConnection=Timestamp[-1], avg_conn_count=toint(series_stats_ConnCounts_avg)
| project-reorder FirstConnection, LastConnection, DeviceName, InitiatingProcessFileName, RemoteIP, count_of_hours, inliner_count, outlier_count, avg_conn_count

Stages and Predicates

Parameters

let lookback = 3d;
let min_uniform_count = 4 * (lookback / 1d);

Stage 1: source

DeviceNetworkEvents

Stage 2: where

| where Timestamp > ago(lookback)

Stage 3: where

| where ActionType endswith "AggregatedReport"

Stage 4: where

| where ipv4_is_private(RemoteIP) == false

Stage 5: extend

| extend ConnectionCount = toint(parse_json(AdditionalFields).uniqueEventsAggregated)

Stage 6: project

| project Timestamp = bin(Timestamp, 1h), DeviceName, InitiatingProcessFileName, RemoteIP, ConnectionCount

Stage 7: sort

| sort by Timestamp asc

Stage 8: summarize

| summarize Timestamp = make_list(Timestamp), ConnCounts = make_list(ConnectionCount) by DeviceName, InitiatingProcessFileName, RemoteIP

Stage 9: extend

| extend count_of_hours = array_length(Timestamp)

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 10: extend

| extend anomalies_decomposed = series_decompose_anomalies(ConnCounts, 1.5, -1),
        series_stats(ConnCounts)

Stage 11: kusto:mv-apply

| mv-apply anomaly = anomalies_decomposed to typeof(int) on (
                summarize inliner_count = countif(anomaly !in (-1, 1)), outlier_count = countif(anomaly in (-1, 1))
    )

Stage 12: where

| where inliner_count >= min_uniform_count and series_stats_ConnCounts_avg > 45

Stage 13: sort

| sort by series_stats_ConnCounts_avg desc

Stage 14: extend

| extend FirstConnection = Timestamp[0], LastConnection=Timestamp[-1], avg_conn_count=toint(series_stats_ConnCounts_avg)

Stage 15: project-reorder

| project-reorder FirstConnection, LastConnection, DeviceName, InitiatingProcessFileName, RemoteIP, count_of_hours, inliner_count, outlier_count, avg_conn_count

Exclusions

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

FieldKindExcluded values
RemoteIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8

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
ActionTypeends_with
  • AggregatedReport
inliner_countge
  • min_uniform_count transforms: cased
series_stats_ConnCounts_avggt
  • 45 transforms: cased

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
ConnCountssummarize
DeviceNamesummarize
InitiatingProcessFileNamesummarize
RemoteIPsummarize
Timestampsummarize
count_of_hoursextend
anomalies_decomposedextend
FirstConnectionextend
LastConnectionextend
avg_conn_countextend