Detection rules › Kusto
New country signIn with correct password
Identifies an interrupted sign-in session from a country the user has not sign-in before in the last 7 days, where the password was correct. Although the session is interrupted by other controls such as multi factor authentication or conditional access policies, the user credentials should be reset due to logs indicating a correct password was observed during sign-in.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Successful sign-in |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
Rule body
id: 7808c05a-3afd-4d13-998a-a59e2297693f
name: New country signIn with correct password
description: |
'Identifies an interrupted sign-in session from a country the user has not sign-in before in the last 7 days, where the password was correct. Although the session is interrupted by other controls such as multi factor authentication or conditional access policies, the user credentials should be reset due to logs indicating a correct password was observed during sign-in.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
query: |
// Creating a list of successful sign-in by users in the last 7 days.
let KnownUserCountry = (
SigninLogs
| where TimeGenerated between (ago(7d) .. ago(1d) )
| where ResultType == 0
| summarize KnownCountry = make_set(Location,1048576) by UserPrincipalName
);
// Identify sign-ins that are no successful but have the auth details indicating a correct password.
SigninLogs
| where TimeGenerated >= ago(1d)
| where ResultType != 0
| extend ParseAuth = parse_json(AuthenticationDetails)
| extend AuthMethod = tostring(ParseAuth.[0].authenticationMethod),
PasswordResult = tostring(ParseAuth.[0].authenticationStepResultDetail),
AuthSucceeded = tostring(ParseAuth.[0].succeeded)
| where PasswordResult == "Correct Password" or AuthSucceeded == "true"
| where AuthMethod == "Password"
| extend failureReason = tostring(Status.failureReason)
| summarize NewCountry = make_set(Location,1048576), LastObservedTime = max(TimeGenerated), AppName = make_set(AppDisplayName,1048576) by UserPrincipalName, PasswordResult, AuthSucceeded, failureReason
// Combining both tables by user
| join kind=inner KnownUserCountry on UserPrincipalName
// Compare both arrays and identify if the country has been observed in the past.
| extend CountryDiff = set_difference(NewCountry,KnownCountry)
| extend CountryDiffCount = array_length(CountryDiff)
// Count the new column to only alert if there is a difference between both arrays
| where CountryDiffCount != 0
| extend NewCountryEvent = CountryDiff
// Getting UserName and Domain
| extend Name = split(UserPrincipalName,"@",0),
Domain = split(UserPrincipalName,"@",1)
| mv-expand Name,Domain
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: Domain
customDetails:
LastObservedTime: LastObservedTime
AppName: AppName
NewCountryEvent: NewCountryEvent
PasswordResult: PasswordResult
AuthSucceeded: AuthSucceeded
failureReason: failureReason
eventGroupingSettings:
aggregationKind: SingleAlert
version: 1.0.2
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Juanse
support:
tier: Community
categories:
domains: [ "Identity", "Security - Threat Protection" ]
Stages and Predicates
Let binding: KnownUserCountry
let KnownUserCountry = (
SigninLogs
| where TimeGenerated between (ago(7d) .. ago(1d) )
| where ResultType == 0
| summarize KnownCountry = make_set(Location,1048576) by UserPrincipalName
);
Stage 1: source
SigninLogs
Stage 2: where
| where TimeGenerated >= ago(1d)
Stage 3: where
| where ResultType != 0
Stage 4: extend
| extend ParseAuth = parse_json(AuthenticationDetails)
Stage 5: extend
| extend AuthMethod = tostring(ParseAuth.[0].authenticationMethod),
PasswordResult = tostring(ParseAuth.[0].authenticationStepResultDetail),
AuthSucceeded = tostring(ParseAuth.[0].succeeded)
Stage 6: where
| where PasswordResult == "Correct Password" or AuthSucceeded == "true"
Stage 7: where
| where AuthMethod == "Password"
Stage 8: extend
| extend failureReason = tostring(Status.failureReason)
Stage 9: summarize
| summarize NewCountry = make_set(Location,1048576), LastObservedTime = max(TimeGenerated), AppName = make_set(AppDisplayName,1048576) by UserPrincipalName, PasswordResult, AuthSucceeded, failureReason
Stage 10: join
| join kind=inner KnownUserCountry on UserPrincipalName
Stage 11: extend
| extend CountryDiff = set_difference(NewCountry,KnownCountry)
Stage 12: extend
| extend CountryDiffCount = array_length(CountryDiff)
Stage 13: where
| where CountryDiffCount != 0
Stage 14: extend
| extend NewCountryEvent = CountryDiff
Stage 15: extend
| extend Name = split(UserPrincipalName,"@",0),
Domain = split(UserPrincipalName,"@",1)
Stage 16: mv-expand
| mv-expand Name,Domain
Indicators
These rows show field, operator, and value matches.
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppName | summarize |
AuthSucceeded | summarize |
LastObservedTime | summarize |
NewCountry | summarize |
PasswordResult | summarize |
UserPrincipalName | summarize |
failureReason | summarize |
CountryDiff | extend |
CountryDiffCount | extend |
NewCountryEvent | extend |
Domain | extend |
Name | extend |