← Back to SOC feed Coverage →

New Location Sign in with Mail forwarding activity

kql MEDIUM Azure-Sentinel
T1114T1020T1078
OfficeActivitySigninLogs
huntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-06-03T11:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may be using new locations to sign in and forward user emails as part of credential compromise or phishing campaigns. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential account takeover or lateral movement attempts.

KQL Query

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let lookback = starttime - 14d;
let countThreshold = 1;
SigninLogs
| where TimeGenerated between(starttime..endtime)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), perIdentityAuthCount = count()
by UserPrincipalName, locationString = strcat(tostring(LocationDetails["countryOrRegion"]), "/", tostring(LocationDetails["state"]), "/",
tostring(LocationDetails["city"]), ";" , tostring(LocationDetails["geoCoordinates"]))
| summarize StartTime = min(StartTimeUtc), EndTime = max(EndTimeUtc), distinctAccountCount = count(), identityList=makeset(UserPrincipalName) by locationString
| extend identityList = iff(distinctAccountCount<10, identityList, "multiple (>10)")
| join kind= anti (
SigninLogs
| where TimeGenerated between(lookback..starttime)
| project locationString= strcat(tostring(LocationDetails["countryOrRegion"]), "/", tostring(LocationDetails["state"]), "/",
tostring(LocationDetails["city"]), ";" , tostring(LocationDetails["geoCoordinates"]))
| summarize priorCount = count() by locationString
)
on locationString
| where distinctAccountCount > countThreshold
| mv-expand todynamic(identityList)
| extend timestamp = StartTime, AccountCustomEntity = identityList
| extend AccountCustomEntity = tostring(AccountCustomEntity)
| join kind=inner
(
 OfficeActivity
| where (Operation =~ "Set-Mailbox" and Parameters contains 'ForwardingSmtpAddress')
or (Operation in~ ('New-InboxRule','Set-InboxRule') and (Parameters contains 'ForwardTo' or Parameters contains 'RedirectTo'))
| extend parsed=parse_json(Parameters)
| extend fwdingDestination_initial = (iif(Operation=~"Set-Mailbox", tostring(parsed[1].Value), tostring(parsed[2].Value)))
| where isnotempty(fwdingDestination_initial)
| extend fwdingDestination = iff(fwdingDestination_initial has "smtp", (split(fwdingDestination_initial,":")[1]), fwdingDestination_initial )
| parse fwdingDestination with * '@' ForwardedtoDomain
| parse UserId with *'@' UserDomain
| extend subDomain = ((split(strcat(tostring(split(UserDomain, '.')[-2]),'.',tostring(split(UserDomain, '.')[-1])), '.') [0]))
| where ForwardedtoDomain !contains subDomain
| extend Result = iff( ForwardedtoDomain != UserDomain ,"Mailbox rule created to forward to External Domain", "Forward rule for Internal domain")
| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| project TimeGenerated, UserId, UserDomain, subDomain, Operation, ForwardedtoDomain, ClientIPAddress, Result, OriginatingServer, OfficeObjectId, fwdingDestination
| extend timestamp = TimeGenerated, AccountCustomEntity = UserId, IPCustomEntity = ClientIPAddress, HostCustomEntity = OriginatingServer
| extend AccountCustomEntity = tostring(AccountCustomEntity)
) on AccountCustomEntity

Analytic Rule Definition

id: a689a21c-9369-47e6-b5fa-e1f65045c1cf
name: New Location Sign in with Mail forwarding activity
description: |
  'This query helps detect new Microsoft Entra ID sign in from a new location correlating with Office Activity data highlighting cases where user mails are being forwarded and shows if  it is being forwarded to external domains as well.'
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (Exchange)
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
tactics:
  - Collection
  - Exfiltration
  - InitialAccess
relevantTechniques:
  - T1114
  - T1020
  - T1078
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  let lookback = starttime - 14d;
  let countThreshold = 1;
  SigninLogs
  | where TimeGenerated between(starttime..endtime)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), perIdentityAuthCount = count()
  by UserPrincipalName, locationString = strcat(tostring(LocationDetails["countryOrRegion"]), "/", tostring(LocationDetails["state"]), "/",
  tostring(LocationDetails["city"]), ";" , tostring(LocationDetails["geoCoordinates"]))
  | summarize StartTime = min(StartTimeUtc), EndTime = max(EndTimeUtc), distinctAccountCount = count(), identityList=makeset(UserPrincipalName) by locationString
  | extend identityList = iff(distinctAccountCount<10, identityList, "multiple (>10)")
  | join kind= anti (
  SigninLogs
  | where TimeGenerated between(lookback..starttime)
  | project locationString= strcat(tostring(LocationDetails["countryOrRegion"]), "/", tostring(LocationDetails["state"]), "/",
  tostring(LocationDetails["city"]), ";" , tostring(LocationDetails["geoCoordinates"]))
  | summarize priorCount = count() by locationString
  )
  on locationString
  | where distinctAccountCount > countThreshold
  | mv-expand todynamic(identityList)
  | extend timestamp = StartTime, AccountCustomEntity = identityList
  | extend AccountCustomEntity = tostring(AccountCustomEntity)
  | join kind=inner
  (
   OfficeActivity
  | where (Operation =~ "Set-Mailbox" and Parameters contains 'ForwardingSmtpAddress')
  or (Operation in~ ('New-InboxRule','Set-InboxRule') and (Parameters contains 'ForwardTo' or Parameters contains 'RedirectTo'))
  | extend parsed=parse_json(Parameters)
  | extend fwdingDestination_initial = (iif(Operation=~"Set-Mailbox", tostring(parsed[1].Value), tostring(parsed[2].Value)))
  | where isnotempty(fwdingDestination_initial)
  | extend fwdingDestination = iff(fwdingDestination_initial has "smtp", (split(fwdingDestination_initial,":")[1]), fwdingDestination_initial )
  | parse fwdingDestination with * '@' ForwardedtoDomain
  | parse UserId with *'@' UserDomain
  | extend subDomain = ((split(strcat(tostring(split(UserDomain, '.')[-2]),'.',tostring(split(UserDomain, '.')[-1])), '.') [0]))
  | where ForwardedtoDomain !contains subDomain
  | extend Result = iff( ForwardedtoDomain != UserDomain ,"Mai

Required Data Sources

Sentinel TableNotes
OfficeActivityEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/MultipleDataSources/MailForwardingActivityFromNewLocation.yaml