Credential Access · Collection
T1557 — Adversary-in-the-Middle
Adversaries may attempt to position themselves between two or more networked devices using an adversary-in-the-middle (AiTM) technique to support follow-on behaviors such as [Network Sniffing](https://attack.mitre.org/techniques/T1040), [Transmitted Data Manipulation](https://attack.mitre.org/techniques/T1565/002), or replay attacks ([Exploitation for Credential Access](https://attack.mitre.org/techniques/T1212))...
Investigate Adversary-in-the-Middle activity in the context of Credential Access/Collection: confirm scope, affected host/identity, and whether it matches expected administrative behaviour before deciding this is benign.
Platforms
Linux, macOS, Network Devices, Windows
Priority / status
high / draft
Evidence to collect
- Host or resource affected
- Account or identity involved
- Timestamp of the activity
- Related process, file, or network artifact
- Any preceding or follow-on alerts
D3-UGLPA · detect
User Geolocation Logon Pattern Analysis
Monitor for User Geolocation Logon Pattern Analysis indicators relevant to this technique.
Tooling: Defender for Endpoint
D3-NTF · isolate
Network Traffic Filtering
Apply Network Traffic Filtering to contain the blast radius once this technique is observed.
Tooling: Sentinel, Defender for Endpoint
| ATT&CK Technique | D3FEND Technique | Practical SOC Action | Tooling |
|---|---|---|---|
| T1557 Adversary-in-the-Middle | User Geolocation Logon Pattern Analysis | Monitor for User Geolocation Logon Pattern Analysis indicators relevant to this technique. | Defender for Endpoint |
| T1557 Adversary-in-the-Middle | Network Traffic Filtering | Apply Network Traffic Filtering to contain the blast radius once this technique is observed. | Sentinel, Defender for Endpoint |
T1557 Adversary-in-the-Middle → D3FEND → SOC action
Investigation steps — Microsoft
- Review Defender for Endpoint / Defender XDR alerts and timeline for the affected host or identity.
- Check Sentinel analytics rules and incidents correlated with this technique.
- Review Entra ID sign-in and audit logs if the technique involves an identity or cloud resource.
Investigation steps — generic
- Confirm whether the observed adversary-in-the-middle activity matches expected administrative or application behaviour.
- Identify the host, account, or resource where the activity occurred and its business criticality.
- Check for related alerts before and after this activity to reconstruct the broader intrusion timeline.
- Real detection reference: "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.
Response actions
| Action | Risk | Automation safe | Approval required |
|---|---|---|---|
| Contain the affected host or account | Medium | No | Yes |
| Collect and preserve evidence | Low | Yes | No |
KQL
1c08a7cb-7ff4-5a37-a961-39c29f8d07bd — Whisper Security - BGP Route Anomaly with Traffic Spike
// 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 3f0c20d5-6228-48ef-92f3-9ff7822c1954 — A host is potentially running a hacking tool (ASIM Web Session schema)
let threatCategory="Hacking Tool";
let knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)
[ @"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv"]
with(format="csv", ignoreFirstRecord=True));
let knownUserAgents=toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let customUserAgents=toscalar(_GetWatchlist("UnusualUserAgents") | where SearchKey==threatCategory | extend UserAgent=column_ifexists("UserAgent","") | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let fullUAList = array_concat(knownUserAgents,customUserAgents);
_Im_WebSession(httpuseragent_has_any=fullUAList)
| project SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername
| extend AccountName = tostring(split(SrcUsername, "@")[0]), AccountUPNSuffix = tostring(split(SrcUsername, "@")[1]) ef88eb96-861c-43a0-ab16-f3835a97c928 — Powershell Empire Cmdlets Executed in Command Line
let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt"] with (format="txt")));
(union isfuzzy=true
(SecurityEvent
| where EventID == 4688
//consider filtering on filename if perf issues occur
//where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
// Note: currently the base64_decode_tostring function is limited to supporting UTF8
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
),
(WindowsEvent
| where EventID == 4688
| where EventData has_any ("-encodedCommand", "powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))
//consider filtering on filename if perf issues occur
//extend NewProcessName = tostring(EventData.NewProcessName)
//extend Process=tostring(split(NewProcessName, '\\')[-1])
//FileName = Process
//where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
// Note: currently the base64_decode_tostring function is limited to supporting UTF8
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
)) Escalation criteria
- Adversary-in-the-Middle activity observed on a privileged account or critical system.
- Activity follows or precedes other suspicious behaviour in the same investigation.
- Automated triage cannot confidently rule out malicious intent.
False positive considerations
- Legitimate administrative or maintenance activity matching this pattern.
- Approved security testing or red team exercise.
- Known benign software producing similar telemetry.
# T1557 - Adversary-in-the-Middle
## SOC Recommendation
Investigate Adversary-in-the-Middle activity in the context of Credential Access/Collection: confirm scope, affected host/identity, and whether it matches expected administrative behaviour before deciding this is benign.
## D3FEND Mappings
| D3FEND Technique | Relationship | Practical SOC Action |
|---|---|---|
| User Geolocation Logon Pattern Analysis | Detect | Monitor for User Geolocation Logon Pattern Analysis indicators relevant to this technique. |
| Network Traffic Filtering | Isolate | Apply Network Traffic Filtering to contain the blast radius once this technique is observed. |
## Investigation Steps
1. Review Defender for Endpoint / Defender XDR alerts and timeline for the affected host or identity.
2. Check Sentinel analytics rules and incidents correlated with this technique.
3. Review Entra ID sign-in and audit logs if the technique involves an identity or cloud resource.
## Evidence to Collect
- Host or resource affected
- Account or identity involved
- Timestamp of the activity
- Related process, file, or network artifact
- Any preceding or follow-on alerts
## Response Actions
| Action | Risk | Automation Safe | Approval Required |
|---|---|---|---|
| Contain the affected host or account | Medium | No | Yes |
| Collect and preserve evidence | Low | Yes | No |
## KQL
```kql
// 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
```
```kql
let threatCategory="Hacking Tool";
let knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)
[ @"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv"]
with(format="csv", ignoreFirstRecord=True));
let knownUserAgents=toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let customUserAgents=toscalar(_GetWatchlist("UnusualUserAgents") | where SearchKey==threatCategory | extend UserAgent=column_ifexists("UserAgent","") | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let fullUAList = array_concat(knownUserAgents,customUserAgents);
_Im_WebSession(httpuseragent_has_any=fullUAList)
| project SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername
| extend AccountName = tostring(split(SrcUsername, "@")[0]), AccountUPNSuffix = tostring(split(SrcUsername, "@")[1])
```
```kql
let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt"] with (format="txt")));
(union isfuzzy=true
(SecurityEvent
| where EventID == 4688
//consider filtering on filename if perf issues occur
//where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
// Note: currently the base64_decode_tostring function is limited to supporting UTF8
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
),
(WindowsEvent
| where EventID == 4688
| where EventData has_any ("-encodedCommand", "powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))
//consider filtering on filename if perf issues occur
//extend NewProcessName = tostring(EventData.NewProcessName)
//extend Process=tostring(split(NewProcessName, '\\')[-1])
//FileName = Process
//where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
// Note: currently the base64_decode_tostring function is limited to supporting UTF8
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
))
```
## Escalation Criteria
- Adversary-in-the-Middle activity observed on a privileged account or critical system.
- Activity follows or precedes other suspicious behaviour in the same investigation.
- Automated triage cannot confidently rule out malicious intent.
## False Positive Considerations
- Legitimate administrative or maintenance activity matching this pattern.
- Approved security testing or red team exercise.
- Known benign software producing similar telemetry.
Generated by SOC Response Atlas by Basyrix.
Want to push this directly to Confluence? Upgrade to Basyrix Pro.
- /api/techniques/T1557.json
- /api/recommendations/T1557.json
- /api/d3fend/T1557.json
- /api/mappings/T1557.json
- /api/confluence/T1557.md
Example curl:
curl https://atlas.basyrix.com/api/recommendations/T1557.json Response:
{
"technique_id": "T1557",
"name": "Adversary-in-the-Middle",
"priority": "high",
"status": "draft",
"version": "0.1.0",
"last_reviewed": "2026-07-23",
"generated_by": "SOC Response Atlas by Basyrix",
"tactics": [
"Credential Access",
"Collection"
],
"platforms": [
"Linux",
"macOS",
"Network Devices",
"Windows"
],
"summary": "Adversaries may attempt to position themselves between two or more networked devices using an adversary-in-the-middle (AiTM) technique to support follow-on behaviors such as [Network Sniffing](https://attack.mitre.org/techniques/T1040), [Transmitted Data Manipulation](https://attack.mitre.org/techniques/T1565/002), or replay attacks ([Exploitation for Credential Access](https://attack.mitre.org/techniques/T1212))...",
"soc_recommendation": "Investigate Adversary-in-the-Middle activity in the context of Credential Access/Collection: confirm scope, affected host/identity, and whether it matches expected administrative behaviour before deciding this is benign.",
"d3fend_mappings": [
{
"id": "D3-UGLPA",
"name": "User Geolocation Logon Pattern Analysis",
"relationship": "detect",
"practical_action": "Monitor for User Geolocation Logon Pattern Analysis indicators relevant to this technique.",
"tooling": [
"Defender for Endpoint"
]
},
{
"id": "D3-NTF",
"name": "Network Traffic Filtering",
"relationship": "isolate",
"practical_action": "Apply Network Traffic Filtering to contain the blast radius once this technique is observed.",
"tooling": [
"Sentinel",
"Defender for Endpoint"
]
}
],
"investigation_steps": {
"microsoft": [
"Review Defender for Endpoint / Defender XDR alerts and timeline for the affected host or identity.",
"Check Sentinel analytics rules and incidents correlated with this technique.",
"Review Entra ID sign-in and audit logs if the technique involves an identity or cloud resource."
],
"generic": [
"Confirm whether the observed adversary-in-the-middle activity matches expected administrative or application behaviour.",
"Identify the host, account, or resource where the activity occurred and its business criticality.",
"Check for related alerts before and after this activity to reconstruct the broader intrusion timeline.",
"Real detection reference: \"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."
]
},
"evidence_to_collect": [
"Host or resource affected",
"Account or identity involved",
"Timestamp of the activity",
"Related process, file, or network artifact",
"Any preceding or follow-on alerts"
],
"response_actions": [
{
"name": "Contain the affected host or account",
"category": "Containment",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Endpoint / Entra ID",
"notes": "Requires approval -- confirm malicious intent before isolating a host or disabling an account."
},
{
"name": "Collect and preserve evidence",
"category": "Investigation",
"risk": "Low",
"automation_safe": true,
"approval_required": false,
"tool": "Defender for Endpoint",
"notes": "Safe -- read-only evidence collection."
}
],
"queries": {
"kql": [
{
"name": "1c08a7cb-7ff4-5a37-a961-39c29f8d07bd — 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. (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed).)",
"query": "// MITRE ATT&CK: T1557 - Adversary-in-the-Middle\n// Tactic: Collection\n// Detects BGP route origin changes correlated with traffic volume anomalies\nlet lookbackPeriod = 1d;\nlet bgpChanges = WhisperHistory_CL\n | where TimeGenerated > ago(lookbackPeriod)\n | where isnotempty(bgpOrigin)\n | serialize\n | extend PreviousOrigin = prev(bgpOrigin), PreviousIndicator = prev(indicator)\n | where indicator == PreviousIndicator\n | where bgpOrigin != PreviousOrigin\n | where isnotempty(PreviousOrigin)\n | project indicator, bgpPrefix, OldOrigin = PreviousOrigin, NewOrigin = bgpOrigin, ChangeTime = snapshotDate;\nlet CommonSecurityLogSafe = union isfuzzy=true CommonSecurityLog, (datatable(TimeGenerated:datetime, DestinationIP:string, SentBytes:long, ReceivedBytes:long)[]);\nlet trafficBaseline = CommonSecurityLogSafe\n | where TimeGenerated > ago(lookbackPeriod)\n | where isnotempty(DestinationIP)\n | summarize BaselineBytes = avg(SentBytes + ReceivedBytes), BaselineCount = count() by DestinationIP, bin(TimeGenerated, 1h)\n | summarize AvgBaselineBytes = avg(BaselineBytes), AvgBaselineCount = avg(BaselineCount) by DestinationIP;\nlet recentTraffic = CommonSecurityLogSafe\n | where TimeGenerated > ago(15m)\n | where isnotempty(DestinationIP)\n | summarize RecentBytes = sum(SentBytes + ReceivedBytes), RecentCount = count() by DestinationIP;\nrecentTraffic\n | join kind=inner (trafficBaseline) on DestinationIP\n | where RecentBytes > AvgBaselineBytes * 3 or RecentCount > AvgBaselineCount * 3\n | extend TrafficChange = iff(AvgBaselineBytes > 0, round((RecentBytes - AvgBaselineBytes) / AvgBaselineBytes * 100, 2), 0.0)\n | join kind=inner (bgpChanges) on $left.DestinationIP == $right.indicator\n | project TimeGenerated = ChangeTime, IPAddress = DestinationIP, Prefix = bgpPrefix, OriginASN = NewOrigin, OldOriginASN = OldOrigin, TrafficChange, RecentBytes, AvgBaselineBytes"
},
{
"name": "3f0c20d5-6228-48ef-92f3-9ff7822c1954 — A host is potentially running a hacking tool (ASIM Web Session schema)",
"description": "This rule identifies a web request with a user agent header known to belong to a hacking tool. This indicates a hacking tool is used on the host.<br>You can add custom hacking tool indicating User-Agent headers using a watchlist, for more information refer to the [UnusualUserAgents Watchlist](https://aka.ms/ASimUnusualUserAgentsWatchlist). This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)' (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed).)",
"query": "let threatCategory=\"Hacking Tool\";\nlet knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)\n [ @\"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv\"] \n with(format=\"csv\", ignoreFirstRecord=True));\nlet knownUserAgents=toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));\nlet customUserAgents=toscalar(_GetWatchlist(\"UnusualUserAgents\") | where SearchKey==threatCategory | extend UserAgent=column_ifexists(\"UserAgent\",\"\") | where isnotempty(UserAgent) | summarize make_list(UserAgent));\nlet fullUAList = array_concat(knownUserAgents,customUserAgents);\n_Im_WebSession(httpuseragent_has_any=fullUAList)\n| project SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername\n| extend AccountName = tostring(split(SrcUsername, \"@\")[0]), AccountUPNSuffix = tostring(split(SrcUsername, \"@\")[1])"
},
{
"name": "ef88eb96-861c-43a0-ab16-f3835a97c928 — Powershell Empire Cmdlets Executed in Command Line",
"description": "This query identifies use of PowerShell Empire's cmdlets within the command line data of the PowerShell process, indicating potential use of the post-exploitation tool.' (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed), mapped via sub-technique T1557.001.)",
"query": "let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@\"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt\"] with (format=\"txt\")));\n(union isfuzzy=true\n (SecurityEvent\n| where EventID == 4688\n//consider filtering on filename if perf issues occur\n//where FileName in~ (\"powershell.exe\",\"powershell_ise.exe\",\"pwsh.exe\")\n| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))\n| where CommandLine has \"-encodedCommand\"\n| parse kind=regex flags=i CommandLine with * \"-EncodedCommand \" encodedCommand\n| extend encodedCommand = iff(encodedCommand has \" \", tostring(split(encodedCommand, \" \")[0]), encodedCommand)\n// Note: currently the base64_decode_tostring function is limited to supporting UTF8\n| extend decodedCommand = translate('\\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)\n| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)\n| where EfectiveCommand matches regex regexEmpire\n| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName\n| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')\n),\n(WindowsEvent\n| where EventID == 4688\n| where EventData has_any (\"-encodedCommand\", \"powershell.exe\",\"powershell_ise.exe\",\"pwsh.exe\")\n| where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))\n//consider filtering on filename if perf issues occur\n//extend NewProcessName = tostring(EventData.NewProcessName)\n//extend Process=tostring(split(NewProcessName, '\\\\')[-1])\n//FileName = Process\n//where FileName in~ (\"powershell.exe\",\"powershell_ise.exe\",\"pwsh.exe\")\n| extend ParentProcessName = tostring(EventData.ParentProcessName)\n| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))\n| extend CommandLine = tostring(EventData.CommandLine)\n| where CommandLine has \"-encodedCommand\"\n| parse kind=regex flags=i CommandLine with * \"-EncodedCommand \" encodedCommand\n| extend encodedCommand = iff(encodedCommand has \" \", tostring(split(encodedCommand, \" \")[0]), encodedCommand)\n// Note: currently the base64_decode_tostring function is limited to supporting UTF8\n| extend decodedCommand = translate('\\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)\n| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)\n| where EfectiveCommand matches regex regexEmpire\n| extend SubjectUserName = tostring(EventData.SubjectUserName)\n| extend SubjectDomainName = tostring(EventData.SubjectDomainName)\n| extend NewProcessName = tostring(EventData.NewProcessName)\n| extend Process=tostring(split(NewProcessName, '\\\\')[-1])\n| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName\n| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')\n))"
}
],
"spl": [],
"esql": [
{
"name": "bc9f5144-0ead-476e-ba6e-cef295601195 — Microsoft Entra ID Impossible Travel Sign-in",
"description": "(ESQL) Detects successful Microsoft Entra ID interactive sign-ins for the same user from two geographically separated locations within a 90-minute window, where the implied travel speed between the two points exceeds what is physically possible (>=800 km/h, faster than modern commercial airliners) and the geographic separation is at least 500 km... (Source: Elastic's official detection-rules repository (Elastic License v2).)",
"query": "// successful interactive sign-ins with country + region populated.\nfrom logs-azure.signinlogs-*\n| where data_stream.dataset == \"azure.signinlogs\"\n and event.outcome == \"success\"\n and azure.signinlogs.category == \"SignInLogs\"\n and azure.signinlogs.properties.user_principal_name is not null\n and source.geo.location is not null\n and source.geo.region_name is not null\n\n| eval Esql.source_geo_lat = st_y(source.geo.location),\n Esql.source_geo_lon = st_x(source.geo.location)\n\n// collapse each (user, country, region) into one centroid + the actual lat/lon\n// of the first and last event in that region. FIRST/LAST lock coords to the\n// timestamp ordering so we can later build the honest event pair.\n| stats\n Esql.region_centroid_lat = avg(Esql.source_geo_lat),\n Esql.region_centroid_lon = avg(Esql.source_geo_lon),\n Esql.region_first_lat = first(Esql.source_geo_lat, @timestamp),\n Esql.region_first_lon = first(Esql.source_geo_lon, @timestamp),\n Esql.region_last_lat = last(Esql.source_geo_lat, @timestamp),\n Esql.region_last_lon = last(Esql.source_geo_lon, @timestamp),\n Esql.region_first_seen = min(@timestamp),\n Esql.region_last_seen = max(@timestamp),\n Esql.region_event_count = count(*),\n Esql.region_city_values = values(source.geo.city_name),\n Esql.region_asn_values = values(source.`as`.organization.name),\n Esql.region_ip_values = values(source.ip),\n Esql.region_ua_values = values(user_agent.original),\n Esql.region_app_id_values = values(azure.signinlogs.properties.app_id),\n Esql.region_app_display_name_values = values(azure.signinlogs.properties.app_display_name),\n Esql.region_client_app_used_values = values(azure.signinlogs.properties.client_app_used),\n Esql.region_resource_id_values = values(azure.signinlogs.properties.resource_id),\n Esql.region_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),\n Esql.region_browser_values = values(azure.signinlogs.properties.device_detail.browser),\n Esql.region_os_values = values(azure.signinlogs.properties.device_detail.operating_system)\n by azure.signinlogs.properties.user_principal_name,\n source.geo.country_name,\n source.geo.region_name\n\n// roll up to the user. two parallel measurements:\n// bbox: corners over region centroids. catches A->B->A because B is still\n// a centroid in the set even when first/last events are in A.\n| stats\n Esql.min_lat = min(Esql.region_centroid_lat),\n Esql.max_lat = max(Esql.region_centroid_lat),\n Esql.min_lon = min(Esql.region_centroid_lon),\n Esql.max_lon = max(Esql.region_centroid_lon),\n Esql.honest_first_lat = first(Esql.region_first_lat, Esql.region_first_seen),\n Esql.honest_first_lon = first(Esql.region_first_lon, Esql.region_first_seen),\n Esql.honest_last_lat = last(Esql.region_last_lat, Esql.region_last_seen),\n Esql.honest_last_lon = last(Esql.region_last_lon, Esql.region_last_seen),\n Esql.timestamp_first_seen = min(Esql.region_first_seen),\n Esql.timestamp_last_seen = max(Esql.region_first_seen), // first arrival in last region > tighter bbox window\n Esql.honest_last_time = max(Esql.region_last_seen), // user's actual last event > honest window\n Esql.region_count = count_distinct(source.geo.region_name),\n Esql.country_count = count_distinct(source.geo.country_name),\n Esql.event_count = sum(Esql.region_event_count),\n Esql.source_geo_country_name_values = values(source.geo.country_name),\n Esql.source_geo_region_name_values = values(source.geo.region_name),\n Esql.source_geo_city_name_values = values(Esql.region_city_values),\n Esql.source_as_organization_name_values = values(Esql.region_asn_values),\n Esql.source_ip_values = values(Esql.region_ip_values),\n Esql.user_agent_original_values = values(Esql.region_ua_values),\n Esql.app_id_values = values(Esql.region_app_id_values),\n Esql.app_display_name_values = values(Esql.region_app_display_name_values),\n Esql.client_app_used_values = values(Esql.region_client_app_used_values),\n Esql.resource_id_values = values(Esql.region_resource_id_values),\n Esql.resource_display_name_values = values(Esql.region_resource_display_name_values),\n Esql.device_detail_browser_values = values(Esql.region_browser_values),\n Esql.device_detail_operating_system_values = values(Esql.region_os_values)\n by azure.signinlogs.properties.user_principal_name\n\n// need at least 2 regions to have anything to compare. cap at 5 because regions\n// are finer-grained than countries (a traveling employee can hit 3-4 in 90m via\n// carrier hub bouncing) > bbox drift stays bounded below this.\n| where Esql.region_count >= 2 and Esql.region_count <= 5\n\n// bbox path (primary trigger): corners over region centroids.\n| eval Esql.p1 = to_geopoint(concat(\"POINT(\", to_string(Esql.min_lon), \" \", to_string(Esql.min_lat), \")\")),\n Esql.p2 = to_geopoint(concat(\"POINT(\", to_string(Esql.max_lon), \" \", to_string(Esql.max_lat), \")\"))\n| eval Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),\n Esql.window_minutes = date_diff(\"minute\", Esql.timestamp_first_seen, Esql.timestamp_last_seen),\n Esql.travel_kmh = case(Esql.window_minutes > 0,\n round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null)\n\n// honest pair (triage signal): real coords at the user's actual first and last\n// events, time locked to those same two events\n| eval Esql.honest_p1 = to_geopoint(concat(\"POINT(\", to_string(Esql.honest_first_lon), \" \", to_string(Esql.honest_first_lat), \")\")),\n Esql.honest_p2 = to_geopoint(concat(\"POINT(\", to_string(Esql.honest_last_lon), \" \", to_string(Esql.honest_last_lat), \")\"))\n| eval Esql.honest_distance_km = round(st_distance(Esql.honest_p1, Esql.honest_p2) / 1000.0, 0),\n Esql.honest_window_minutes = date_diff(\"minute\", Esql.timestamp_first_seen, Esql.honest_last_time),\n Esql.honest_travel_kmh = case(Esql.honest_window_minutes > 0,\n round(Esql.honest_distance_km * 60.0 / Esql.honest_window_minutes, 0), null)\n\n// 500 km separation + faster than a commercial airliner. bbox is the trigger\n// purely as triage signal.\n| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800\n\n| keep azure.signinlogs.properties.user_principal_name,\n Esql.source_geo_country_name_values,\n Esql.source_geo_region_name_values,\n Esql.source_geo_city_name_values,\n Esql.source_as_organization_name_values,\n Esql.source_ip_values,\n Esql.user_agent_original_values,\n Esql.app_id_values,\n Esql.app_display_name_values,\n Esql.client_app_used_values,\n Esql.resource_id_values,\n Esql.resource_display_name_values,\n Esql.device_detail_browser_values,\n Esql.device_detail_operating_system_values,\n Esql.country_count,\n Esql.region_count,\n Esql.event_count,\n Esql.timestamp_first_seen,\n Esql.timestamp_last_seen,\n Esql.window_minutes,\n Esql.distance_km,\n Esql.travel_kmh,\n Esql.honest_distance_km,\n Esql.honest_travel_kmh,\n Esql.honest_window_minutes"
},
{
"name": "aff74d85-5bfa-4ff1-ace2-4e3995a37cfa — Google Workspace Impossible Travel Login",
"description": "(ESQL) Detects successful Google Workspace sign-ins for the same user from two geographically separated locations within a 90-minute window, where the implied travel speed between the two points exceeds what is physically possible (>=800 km/h, faster than modern commercial airliners) and the geographic separation is at least 500 km... (Source: Elastic's official detection-rules repository (Elastic License v2).)",
"query": "// successful Google Workspace logins with country + region populated.\nfrom logs-google_workspace.login-*\n| where event.dataset == \"google_workspace.login\"\n and event.action == \"login_success\"\n and event.outcome == \"success\"\n and user.email is not null\n and source.geo.location is not null\n and source.geo.country_name is not null\n and source.geo.region_name is not null\n| eval Esql.source_geo_lat = st_y(source.geo.location),\n Esql.source_geo_lon = st_x(source.geo.location)\n\n// collapse each (user, country, region) into one centroid + the actual lat/lon\n// of the first and last event in that region. FIRST/LAST lock coords to the\n// timestamp ordering so we can later build the honest event pair.\n| stats\n Esql.region_centroid_lat = avg(Esql.source_geo_lat),\n Esql.region_centroid_lon = avg(Esql.source_geo_lon),\n Esql.region_first_lat = first(Esql.source_geo_lat, @timestamp),\n Esql.region_first_lon = first(Esql.source_geo_lon, @timestamp),\n Esql.region_last_lat = last(Esql.source_geo_lat, @timestamp),\n Esql.region_last_lon = last(Esql.source_geo_lon, @timestamp),\n Esql.region_first_seen = min(@timestamp),\n Esql.region_last_seen = max(@timestamp),\n Esql.region_event_count = count(*),\n Esql.region_city_values = values(source.geo.city_name),\n Esql.region_asn_values = values(source.`as`.organization.name),\n Esql.region_ip_values = values(source.ip)\n by user.email,\n source.geo.country_name,\n source.geo.region_name\n\n// roll up to the user. two parallel measurements:\n// bbox: corners over region centroids.\n// honest: real coords at the user's actual first and last events (nested FIRST/LAST).\n| stats\n Esql.min_lat = min(Esql.region_centroid_lat),\n Esql.max_lat = max(Esql.region_centroid_lat),\n Esql.min_lon = min(Esql.region_centroid_lon),\n Esql.max_lon = max(Esql.region_centroid_lon),\n Esql.honest_first_lat = first(Esql.region_first_lat, Esql.region_first_seen),\n Esql.honest_first_lon = first(Esql.region_first_lon, Esql.region_first_seen),\n Esql.honest_last_lat = last(Esql.region_last_lat, Esql.region_last_seen),\n Esql.honest_last_lon = last(Esql.region_last_lon, Esql.region_last_seen),\n Esql.timestamp_first_seen = min(Esql.region_first_seen),\n Esql.timestamp_last_seen = max(Esql.region_first_seen), // first arrival in last region > tighter bbox window\n Esql.honest_last_time = max(Esql.region_last_seen), // user's actual last event > honest window\n Esql.region_count = count_distinct(source.geo.region_name),\n Esql.country_count = count_distinct(source.geo.country_name),\n Esql.event_count = sum(Esql.region_event_count),\n Esql.source_geo_country_name_values = values(source.geo.country_name),\n Esql.source_geo_region_name_values = values(source.geo.region_name),\n Esql.source_geo_city_name_values = values(Esql.region_city_values),\n Esql.source_as_organization_name_values = values(Esql.region_asn_values),\n Esql.source_ip_values = values(Esql.region_ip_values)\n by user.email\n\n// need at least 2 regions to have anything to compare. cap at 5 because regions\n// are finer-grained than countries (a traveling user can hit 3-4 in 90m via\n// carrier hub bouncing) > bbox drift stays bounded below this.\n| where Esql.region_count >= 2 and Esql.region_count <= 5\n\n// bbox path (primary trigger): corners over region centroids.\n| eval Esql.p1 = to_geopoint(concat(\"POINT(\", to_string(Esql.min_lon), \" \", to_string(Esql.min_lat), \")\")),\n Esql.p2 = to_geopoint(concat(\"POINT(\", to_string(Esql.max_lon), \" \", to_string(Esql.max_lat), \")\"))\n| eval Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),\n Esql.window_minutes = date_diff(\"minute\", Esql.timestamp_first_seen, Esql.timestamp_last_seen),\n Esql.travel_kmh = case(Esql.window_minutes > 0,\n round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null)\n\n// honest pair (triage signal): real coords at the user's actual first and last\n// events, time locked to those same two events.\n| eval Esql.honest_p1 = to_geopoint(concat(\"POINT(\", to_string(Esql.honest_first_lon), \" \", to_string(Esql.honest_first_lat), \")\")),\n Esql.honest_p2 = to_geopoint(concat(\"POINT(\", to_string(Esql.honest_last_lon), \" \", to_string(Esql.honest_last_lat), \")\"))\n| eval Esql.honest_distance_km = round(st_distance(Esql.honest_p1, Esql.honest_p2) / 1000.0, 0),\n Esql.honest_window_minutes = date_diff(\"minute\", Esql.timestamp_first_seen, Esql.honest_last_time),\n Esql.honest_travel_kmh = case(Esql.honest_window_minutes > 0,\n round(Esql.honest_distance_km * 60.0 / Esql.honest_window_minutes, 0), null)\n\n// 500 km separation + faster than a commercial airliner. bbox is the trigger\n// honest fields are kept purely as triage signal.\n| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800\n\n| keep user.email,\n Esql.source_geo_country_name_values,\n Esql.source_geo_region_name_values,\n Esql.source_geo_city_name_values,\n Esql.source_as_organization_name_values,\n Esql.source_ip_values,\n Esql.country_count,\n Esql.region_count,\n Esql.event_count,\n Esql.timestamp_first_seen,\n Esql.timestamp_last_seen,\n Esql.window_minutes,\n Esql.distance_km,\n Esql.travel_kmh,\n Esql.honest_distance_km,\n Esql.honest_travel_kmh,\n Esql.honest_window_minutes"
}
]
},
"automation": {
"safe": [
"Add recommendation as Sentinel incident comment.",
"Run enrichment queries.",
"Create ServiceNow SecOps task."
],
"approval_required": [
"Contain or disable the affected host/account.",
"Any change to production configuration."
]
},
"escalation_criteria": [
"Adversary-in-the-Middle activity observed on a privileged account or critical system.",
"Activity follows or precedes other suspicious behaviour in the same investigation.",
"Automated triage cannot confidently rule out malicious intent."
],
"false_positive_considerations": [
"Legitimate administrative or maintenance activity matching this pattern.",
"Approved security testing or red team exercise.",
"Known benign software producing similar telemetry."
],
"confluence": {
"title": "T1557 - Adversary-in-the-Middle Response Guidance",
"labels": [
"mitre",
"attack",
"d3fend",
"secops",
"basyrix"
],
"sections": [
"summary",
"d3fend_mappings",
"investigation_steps",
"response_actions",
"queries",
"automation",
"escalation_criteria",
"false_positive_considerations"
]
}
}