Detection rules › Kusto
Whisper Security - BGP Route Anomaly with Traffic Spike
Detects BGP origin AS changes for IP prefixes correlated with network traffic volume spikes. May indicate BGP hijacking or adversary-in-the-middle attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Rule body
id: 1c08a7cb-7ff4-5a37-a961-39c29f8d07bd
name: Whisper Security - BGP Route Anomaly with Traffic Spike
description: |
Detects BGP origin AS changes for IP prefixes correlated with network traffic volume spikes. May indicate BGP hijacking or adversary-in-the-middle attacks.
severity: High
status: Available
requiredDataConnectors:
- connectorId: WhisperSecurityConnector
dataTypes:
- WhisperHistory_CL
queryFrequency: 15m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1557
query: |
// MITRE ATT&CK: T1557 - Adversary-in-the-Middle
// Tactic: Collection
// Detects BGP route origin changes correlated with traffic volume anomalies
let lookbackPeriod = 1d;
let bgpChanges = WhisperHistory_CL
| where TimeGenerated > ago(lookbackPeriod)
| where isnotempty(bgpOrigin)
| serialize
| extend PreviousOrigin = prev(bgpOrigin), PreviousIndicator = prev(indicator)
| where indicator == PreviousIndicator
| where bgpOrigin != PreviousOrigin
| where isnotempty(PreviousOrigin)
| project indicator, bgpPrefix, OldOrigin = PreviousOrigin, NewOrigin = bgpOrigin, ChangeTime = snapshotDate;
let CommonSecurityLogSafe = union isfuzzy=true CommonSecurityLog, (datatable(TimeGenerated:datetime, DestinationIP:string, SentBytes:long, ReceivedBytes:long)[]);
let trafficBaseline = CommonSecurityLogSafe
| where TimeGenerated > ago(lookbackPeriod)
| where isnotempty(DestinationIP)
| summarize BaselineBytes = avg(SentBytes + ReceivedBytes), BaselineCount = count() by DestinationIP, bin(TimeGenerated, 1h)
| summarize AvgBaselineBytes = avg(BaselineBytes), AvgBaselineCount = avg(BaselineCount) by DestinationIP;
let recentTraffic = CommonSecurityLogSafe
| where TimeGenerated > ago(15m)
| where isnotempty(DestinationIP)
| summarize RecentBytes = sum(SentBytes + ReceivedBytes), RecentCount = count() by DestinationIP;
recentTraffic
| join kind=inner (trafficBaseline) on DestinationIP
| where RecentBytes > AvgBaselineBytes * 3 or RecentCount > AvgBaselineCount * 3
| extend TrafficChange = iff(AvgBaselineBytes > 0, round((RecentBytes - AvgBaselineBytes) / AvgBaselineBytes * 100, 2), 0.0)
| join kind=inner (bgpChanges) on $left.DestinationIP == $right.indicator
| project TimeGenerated = ChangeTime, IPAddress = DestinationIP, Prefix = bgpPrefix, OriginASN = NewOrigin, OldOriginASN = OldOrigin, TrafficChange, RecentBytes, AvgBaselineBytes
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
customDetails:
Prefix: Prefix
OriginASN: OriginASN
TrafficChange: TrafficChange
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookbackPeriod = 1d;
let CommonSecurityLogSafe and let recentTraffic are inlined into the numbered stages below.
Let binding: bgpChanges
let bgpChanges = WhisperHistory_CL
| where TimeGenerated > ago(lookbackPeriod)
| where isnotempty(bgpOrigin)
| serialize
| extend PreviousOrigin = prev(bgpOrigin), PreviousIndicator = prev(indicator)
| where indicator == PreviousIndicator
| where bgpOrigin != PreviousOrigin
| where isnotempty(PreviousOrigin)
| project indicator, bgpPrefix, OldOrigin = PreviousOrigin, NewOrigin = bgpOrigin, ChangeTime = snapshotDate;
Let binding: trafficBaseline
let trafficBaseline = CommonSecurityLogSafe
| where TimeGenerated > ago(lookbackPeriod)
| where isnotempty(DestinationIP)
| summarize BaselineBytes = avg(SentBytes + ReceivedBytes), BaselineCount = count() by DestinationIP, bin(TimeGenerated, 1h)
| summarize AvgBaselineBytes = avg(BaselineBytes), AvgBaselineCount = avg(BaselineCount) by DestinationIP;
Stage 1: union
union of 2 branches
Stage 2: source
CommonSecurityLog
Stage 3: source
datatable
Stage 4: where
where TimeGenerated > ago(900s)
Stage 5: where
where isnotempty(DestinationIP)
Stage 6: summarize
summarize RecentBytes, RecentCount by DestinationIP
Stage 7: join
join kind=inner (trafficBaseline) on DestinationIP
Stage 8: where
where (RecentBytes > (AvgBaselineBytes * 3) or RecentCount > (AvgBaselineCount * 3))
Stage 9: extend
extend TrafficChange
TrafficChange =if
AvgBaselineBytes > 0round((((RecentBytes - AvgBaselineBytes) / AvgBaselineBytes) * 100), 2)else
0.0Stage 10: join
join kind=inner (bgpChanges) on DestinationIP, indicator
Stage 11: project
project AvgBaselineBytes, IPAddress, OldOriginASN, OriginASN, Prefix, RecentBytes, TimeGenerated, TrafficChange
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationIP | is_not_null | field:"DestinationIP" kind:is_not_null | |
PreviousOrigin | is_not_null | field:"PreviousOrigin" kind:is_not_null | |
RecentBytes | cross_field_compare |
| field:"RecentBytes" kind:cross_field_compare value:"AvgBaselineBytes" |
RecentCount | cross_field_compare |
| field:"RecentCount" kind:cross_field_compare value:"AvgBaselineCount" |
bgpOrigin | cross_field_compare |
| field:"bgpOrigin" kind:cross_field_compare value:"PreviousOrigin" |
bgpOrigin | is_not_null | field:"bgpOrigin" kind:is_not_null | |
indicator | cross_field_compare |
| field:"indicator" kind:cross_field_compare value:"PreviousIndicator" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AvgBaselineBytes | project |
IPAddress | project |
OldOriginASN | project |
OriginASN | project |
Prefix | project |
RecentBytes | project |
TimeGenerated | project |
TrafficChange | project |