Detection rules › Kusto

Correlate Unfamiliar sign-in properties & atypical travel alerts

Status
available
Severity
high
Time window
14d
Group by
AadTenantId, AadUserId, AccountObjectId, AccountTenantId
Source
github.com/Azure/Azure-Sentinel

'The combination of an Unfamiliar sign-in properties alert and an Atypical travel alert about the same user within a +10m or -10m window is considered a high severity incident.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts

Rule body kusto

id: a3df4a32-4805-4c6d-8699-f3c888af2f67
name: Correlate Unfamiliar sign-in properties & atypical travel alerts
description: |
  'The combination of an Unfamiliar sign-in properties alert and an Atypical travel alert about the same user within a +10m or -10m window is considered a high severity incident.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
    // We can use this configuration TimeDeltaInMinutes if you want to chnage the time window that we try to match the alerts
    let TimeDeltaInMinutes = 10;
    let Alert_UnfamiliarSignInProps = 
    SecurityAlert
    | where TimeGenerated > ago(1d)
    | where ProductName =~ "Azure Active Directory Identity Protection"
    | where AlertName =~ "Unfamiliar sign-in properties"
    | mv-expand Entity = todynamic(Entities)
    | where Entity.Type =~ "account"
    | extend AadTenantId = tostring(Entity.AadTenantId)
    | extend AadUserId = tostring(Entity.AadUserId)
    | join kind=inner (
    IdentityInfo
    | distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
    | extend UserName = AccountDisplayName
    | extend UserAccount = AccountUPN
    | where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
    | project AccountTenantId, AccountObjectId, UserAccount, UserName
    )
    on
    $left.AadTenantId == $right.AccountTenantId,
    $left.AadUserId == $right.AccountObjectId
    | extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
    | extend Alert_UnfamiliarSignInProps_Time = TimeGenerated
    | extend Alert_UnfamiliarSignInProps_Name = AlertName
    | extend Alert_UnfamiliarSignInProps_Severity = AlertSeverity
    | project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, UserAccount, UserName
    ;
    let Alert_AtypicalTravels = 
    SecurityAlert
    | where TimeGenerated > ago(1d)
    | where ProductName =~ "Azure Active Directory Identity Protection"
    | where AlertName =~ "Atypical travel"
    | mv-expand Entity = todynamic(Entities)
    | where Entity.Type =~ "account"
    | extend AadTenantId = tostring(Entity.AadTenantId)
    | extend AadUserId = tostring(Entity.AadUserId)
    | join kind=inner (
    IdentityInfo
    | distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
    | extend UserName = AccountDisplayName
    | extend UserAccount = AccountUPN
    | where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
    | project AccountTenantId, AccountObjectId, UserAccount, UserName
    )
    on
    $left.AadTenantId == $right.AccountTenantId,
    $left.AadUserId == $right.AccountObjectId
    | extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
    | extend Alert_AtypicalTravels_Time = TimeGenerated
    | extend Alert_AtypicalTravels_Name = AlertName
    | extend Alert_AtypicalTravels_Severity = AlertSeverity
    | extend ExtendedProperties_json= parse_json(ExtendedProperties)
    | extend CurrentLocation = tostring(ExtendedProperties_json.["Current Location"])
    | extend PreviousLocation = tostring(ExtendedProperties_json.["Previous Location"])
    | extend CurrentIPAddress = tostring(ExtendedProperties_json.["Current IP Address"])
    | extend PreviousIPAddress = tostring(ExtendedProperties_json.["Previous IP Address"])
    | extend Comments = tostring(ExtendedProperties_json.Comments)
    | extend RiskDetail = extract(@"(?i)risk detail:\s*(.+)$", 1, Comments)
    | where isempty(RiskDetail) or RiskDetail !contains "admin"
    | project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, CurrentIPAddress, PreviousIPAddress, CurrentLocation, Comments, PreviousLocation, UserAccount, UserName, CompromisedEntity
    ;
    Alert_UnfamiliarSignInProps
    | join kind=inner Alert_AtypicalTravels on UserAccount
    | where abs(datetime_diff('minute', Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Time)) <= TimeDeltaInMinutes
    | extend TimeDelta = Alert_UnfamiliarSignInProps_Time - Alert_AtypicalTravels_Time
    | project UserAccount, AadUserId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, TimeDelta, CurrentLocation, PreviousLocation, CurrentIPAddress, PreviousIPAddress, Comments, UserName
    | extend UserEmailName = split(UserAccount,'@')[0], UPNSuffix = split(UserAccount,'@')[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserAccount
      - identifier: Name
        columnName: UserEmailName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: AadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CurrentIPAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: PreviousIPAddress
customDetails:
    Alert1_Name: Alert_UnfamiliarSignInProps_Name
    Alert1_Time: Alert_UnfamiliarSignInProps_Time
    Alert1_Severity: Alert_UnfamiliarSignInProps_Severity
    Alert2_Name: Alert_AtypicalTravels_Name
    Alert2_Time: Alert_AtypicalTravels_Time
    Alert2_Severity: Alert_AtypicalTravels_Severity
    TimeDelta: TimeDelta
    CurrentLocation: CurrentLocation
    PreviousLocation: PreviousLocation
    CurrentIPAddress: CurrentIPAddress
    PreviousIPAddress: PreviousIPAddress
version: 1.0.9
kind: Scheduled

Stages and Predicates

Parameters

let TimeDeltaInMinutes = 10;

Let binding: Alert_AtypicalTravels

let Alert_AtypicalTravels = SecurityAlert
| where TimeGenerated > ago(1d)
| where ProductName =~ "Azure Active Directory Identity Protection"
| where AlertName =~ "Atypical travel"
| mv-expand Entity = todynamic(Entities)
| where Entity.Type =~ "account"
| extend AadTenantId = tostring(Entity.AadTenantId)
| extend AadUserId = tostring(Entity.AadUserId)
| join kind=inner (
IdentityInfo
| distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
| extend UserName = AccountDisplayName
| extend UserAccount = AccountUPN
| where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
| project AccountTenantId, AccountObjectId, UserAccount, UserName
)
on
$left.AadTenantId == $right.AccountTenantId,
$left.AadUserId == $right.AccountObjectId
| extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
| extend Alert_AtypicalTravels_Time = TimeGenerated
| extend Alert_AtypicalTravels_Name = AlertName
| extend Alert_AtypicalTravels_Severity = AlertSeverity
| extend ExtendedProperties_json= parse_json(ExtendedProperties)
| extend CurrentLocation = tostring(ExtendedProperties_json.["Current Location"])
| extend PreviousLocation = tostring(ExtendedProperties_json.["Previous Location"])
| extend CurrentIPAddress = tostring(ExtendedProperties_json.["Current IP Address"])
| extend PreviousIPAddress = tostring(ExtendedProperties_json.["Previous IP Address"])
| extend Comments = tostring(ExtendedProperties_json.Comments)
| extend RiskDetail = extract(@"(?i)risk detail:\s*(.+)$", 1, Comments)
| where isempty(RiskDetail) or RiskDetail !contains "admin"
| project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, CurrentIPAddress, PreviousIPAddress, CurrentLocation, Comments, PreviousLocation, UserAccount, UserName, CompromisedEntity;

The stages below define let Alert_UnfamiliarSignInProps (the rule's main pipeline source).

Stage 1: source

SecurityAlert

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where ProductName =~ "Azure Active Directory Identity Protection"

Stage 4: where

| where AlertName =~ "Unfamiliar sign-in properties"

Stage 5: mv-expand

| mv-expand Entity = todynamic(Entities)

Stage 6: where

| where Entity.Type =~ "account"

Stage 7: extend

| extend AadTenantId = tostring(Entity.AadTenantId)

Stage 8: extend

| extend AadUserId = tostring(Entity.AadUserId)

Stage 9: join

| join kind=inner (
IdentityInfo
| distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
| extend UserName = AccountDisplayName
| extend UserAccount = AccountUPN
| where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
| project AccountTenantId, AccountObjectId, UserAccount, UserName
)
on
$left.AadTenantId == $right.AccountTenantId,
$left.AadUserId == $right.AccountObjectId

Stage 10: extend (4 consecutive steps)

| extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
| extend Alert_UnfamiliarSignInProps_Time = TimeGenerated
| extend Alert_UnfamiliarSignInProps_Name = AlertName
| extend Alert_UnfamiliarSignInProps_Severity = AlertSeverity
CompromisedEntity =
if(CompromisedEntity == "N/A" or isempty(CompromisedEntity))UserAccount
elseCompromisedEntity

Stage 11: project

| project AadTenantId, AadUserId, AccountTenantId, AccountObjectId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, UserAccount, UserName

The stages below run on Alert_UnfamiliarSignInProps (the outer pipeline).

Stage 12: join

Alert_UnfamiliarSignInProps
| join kind=inner Alert_AtypicalTravels on UserAccount

Stage 13: where

| where abs(datetime_diff('minute', Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Time)) <= TimeDeltaInMinutes

Stage 14: extend

| extend TimeDelta = Alert_UnfamiliarSignInProps_Time - Alert_AtypicalTravels_Time

Stage 15: project

| project UserAccount, AadUserId, Alert_UnfamiliarSignInProps_Name, Alert_UnfamiliarSignInProps_Severity, Alert_UnfamiliarSignInProps_Time, Alert_AtypicalTravels_Name, Alert_AtypicalTravels_Severity, Alert_AtypicalTravels_Time, TimeDelta, CurrentLocation, PreviousLocation, CurrentIPAddress, PreviousIPAddress, Comments, UserName

Stage 16: extend

| extend UserEmailName = split(UserAccount,'@')[0], UPNSuffix = split(UserAccount,'@')[1]

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
AccountDisplayNameis_not_null
  • (no value, null check)
AlertNameeq
  • Atypical travel
  • Unfamiliar sign-in properties
ProductNameeq
  • Azure Active Directory Identity Protection
RiskDetailis_null
  • (no value, null check)
Typeeq
  • account
UserAccountis_not_null
  • (no value, null check)

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
AadUserIdproject
Alert_AtypicalTravels_Nameproject
Alert_AtypicalTravels_Severityproject
Alert_AtypicalTravels_Timeproject
Alert_UnfamiliarSignInProps_Nameproject
Alert_UnfamiliarSignInProps_Severityproject
Alert_UnfamiliarSignInProps_Timeproject
Commentsproject
CurrentIPAddressproject
CurrentLocationproject
PreviousIPAddressproject
PreviousLocationproject
TimeDeltaproject
UserAccountproject
UserNameproject
UPNSuffixextend
UserEmailNameextend