Detection rules › Kusto

Anomalous sign-in location by user account and authenticating application

Status
available
Severity
medium
Time window
7d
Group by
AppDisplayName, AppId, CountOfLocations, TimeGenerated, UserId, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

'This query over Microsoft Entra ID sign-in considers all user sign-ins for each Microsoft Entra ID application and picks out the most anomalous change in location profile for a user within an individual application.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 7cb8f77d-c52f-4e46-b82f-3cf2e106224a
name: Anomalous sign-in location by user account and authenticating application
description: |
  'This query over Microsoft Entra ID sign-in considers all user sign-ins for each Microsoft Entra ID application and picks out the most anomalous change in location profile for a user within an individual application.
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  // Adjust this figure to adjust how sensitive this detection is
  let sensitivity = 2.5;
  // Adjust this figure to set the value that defines the requested estimation accuracy. The default value is 1. Possible values are 0, 1, 2, 3, 4.
  let dcountAccuracy = 1;
  let AuthEvents = materialize(
  union isfuzzy=True SigninLogs, AADNonInteractiveUserSignInLogs
  | where TimeGenerated between (ago(7d) .. now())
  | where ResultType == 0
  | extend LocationDetails = LocationDetails_dynamic
  | extend Location = strcat(LocationDetails.countryOrRegion, "-", LocationDetails.state,"-", LocationDetails.city)
  | where Location != "--");
  AuthEvents
  | summarize dcount(Location, dcountAccuracy) by AppDisplayName, AppId, UserPrincipalName, UserId, bin(startofday(TimeGenerated), 1d)
  | where dcount_Location > 2
  | make-series CountOfLocations = sum(dcount_Location) on TimeGenerated  step 1d by AppId, UserId
  | extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfLocations, sensitivity, -1, 'linefit')
  | mv-expand CountOfLocations to typeof(double), TimeGenerated to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)
  | where Anomalies > 0 and Baseline > 0
  | join kind=inner( AuthEvents | extend TimeStamp = startofday(TimeGenerated)) on UserId, AppId
  | extend SignInDetails = bag_pack("TimeGenerated", TimeGenerated1, "Location", Location, "Source", IPAddress, "Device", DeviceDetail_dynamic)
  | summarize SignInDetailsSet=make_set(SignInDetails, 1000) by UserId, UserPrincipalName, CountOfLocations, TimeGenerated, AppId, AppDisplayName
  | extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId   
eventGroupingSettings:
  aggregationKind: SingleAlert
customDetails:
  Application: AppDisplayName
alertDetailsOverride:
  alertDisplayNameFormat: Anomalous sign-in location by {{UserPrincipalName}} to {{AppDisplayName}}
  alertDescriptionFormat: |
    This query over Microsoft Entra ID sign-in considers all user sign-ins for each Microsoft Entra ID application and picks out the most anomalous change in location profile for a user within an
    individual application. This has detected {{UserPrincipalName}} signing into {{AppDisplayName}} from {{CountOfLocations}} 
    different locations.
version: 2.0.5
kind: Scheduled

Stages and Predicates

Stage 0: let

let sensitivity = 2.5;
let dcountAccuracy = 1;
let AuthEvents = materialize(<inlined as stages below>);

Stage 1: union

union of 2 branches

Stage 2: source

SigninLogs

Stage 3: source

AADNonInteractiveUserSignInLogs

Stage 4: where

where TimeGenerated between (ago(604800s) .. now())

Stage 5: where

where ResultType == 0

Stage 6: extend

extend LocationDetails

Stage 7: extend

extend Location

Stage 8: where

where Location !~ "--"

Stage 9: summarize

summarize by AppDisplayName, AppId, UserPrincipalName, UserId

Stage 10: where

where dcount_Location > 2

Stage 11: summarize

summarize CountOfLocations by AppId, UserId

Stage 12: extend

extend Anomalies, Baseline, Score

Stage 13: mv-expand

mv-expand CountOfLocations

Stage 14: where

where Anomalies > 0 and Baseline > 0

Stage 15: join

join kind=inner (AuthEvents) on UserId, AppId

Stage 16: extend

extend SignInDetails

Stage 17: summarize

summarize SignInDetailsSet by UserId, UserPrincipalName, CountOfLocations, TimeGenerated, AppId, AppDisplayName

Stage 18: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNamesummarize
AppIdsummarize
CountOfLocationssummarize
SignInDetailsSetsummarize
TimeGeneratedsummarize
UserIdsummarize
UserPrincipalNamesummarize
Nameextend
UPNSuffixextend