Detection rules › Kusto
Anomalous Single Factor Signin
'Detects successful signins using single factor authentication where the device, location, and ASN are abnormal. Single factor authentications pose an opportunity to access compromised accounts, investigate these for anomalous occurrencess. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-devices#non-compliant-device-sign-in'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Many Failed SignIns (Panther)
- 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)
- Detect non-admin requesting token for admin applications (Kusto)
- Discovery Using AzureHound (Sigma)
Rule body kusto
id: f7c3f5c8-71ea-49ff-b8b3-148f0e346291
name: Anomalous Single Factor Signin
description: |
'Detects successful signins using single factor authentication where the device, location, and ASN are abnormal.
Single factor authentications pose an opportunity to access compromised accounts, investigate these for anomalous occurrencess.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-devices#non-compliant-device-sign-in'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let known_locations = (SigninLogs
| where TimeGenerated between(ago(7d)..ago(1d))
| where ResultType == 0
| extend LocationDetail = strcat(Location, "-", LocationDetails.state)
| summarize by LocationDetail);
let known_asn = (SigninLogs
| where TimeGenerated between(ago(7d)..ago(1d))
| where ResultType == 0
| summarize by AutonomousSystemNumber);
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType == 0
| where isempty(DeviceDetail.deviceId)
| where AuthenticationRequirement == "singleFactorAuthentication"
| extend LocationParsed = parse_json(LocationDetails), DeviceParsed = parse_json(DeviceDetail)
| extend City = tostring(LocationParsed.city), State = tostring(LocationParsed.state)
| extend LocationDetail = strcat(Location, "-", State)
| extend DeviceId = tostring(DeviceParsed.deviceId), DeviceName=tostring(DeviceParsed.displayName), OS=tostring(DeviceParsed.operatingSystem), Browser=tostring(DeviceParsed.browser)
| where AutonomousSystemNumber !in (known_asn) and LocationDetail !in (known_locations)
| project TimeGenerated, Type, UserId, UserDisplayName, UserPrincipalName, IPAddress, Location, State, City, ResultType, ResultDescription, AppId, AppDisplayName, AuthenticationRequirement, ConditionalAccessStatus, ResourceDisplayName, ClientAppUsed, Identity, HomeTenantId, ResourceTenantId, Status, UserAgent, DeviceId, DeviceName, OS, Browser, MfaDetail
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: AppId
- identifier: Name
columnName: AppDisplayName
version: 1.0.6
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Pete Bryan
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Let binding: known_locations
let known_locations = (SigninLogs
| where TimeGenerated between(ago(7d)..ago(1d))
| where ResultType == 0
| extend LocationDetail = strcat(Location, "-", LocationDetails.state)
| summarize by LocationDetail);
Let binding: known_asn
let known_asn = (SigninLogs
| where TimeGenerated between(ago(7d)..ago(1d))
| where ResultType == 0
| summarize by AutonomousSystemNumber);
Stage 1: source
SigninLogs
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where ResultType == 0
Stage 4: where
| where isempty(DeviceDetail.deviceId)
Stage 5: where
| where AuthenticationRequirement == "singleFactorAuthentication"
Stage 6: extend (4 consecutive steps)
| extend LocationParsed = parse_json(LocationDetails), DeviceParsed = parse_json(DeviceDetail)
| extend City = tostring(LocationParsed.city), State = tostring(LocationParsed.state)
| extend LocationDetail = strcat(Location, "-", State)
| extend DeviceId = tostring(DeviceParsed.deviceId), DeviceName=tostring(DeviceParsed.displayName), OS=tostring(DeviceParsed.operatingSystem), Browser=tostring(DeviceParsed.browser)
Stage 7: where
| where AutonomousSystemNumber !in (known_asn) and LocationDetail !in (known_locations)
References known_asn, known_locations (defined above).
Stage 8: project
| project TimeGenerated, Type, UserId, UserDisplayName, UserPrincipalName, IPAddress, Location, State, City, ResultType, ResultDescription, AppId, AppDisplayName, AuthenticationRequirement, ConditionalAccessStatus, ResourceDisplayName, ClientAppUsed, Identity, HomeTenantId, ResourceTenantId, Status, UserAgent, DeviceId, DeviceName, OS, Browser, MfaDetail
Stage 9: extend
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
AutonomousSystemNumber | eq | known_asn |
LocationDetail | eq | known_locations |
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.
| Field | Kind | Values |
|---|---|---|
AuthenticationRequirement | eq |
|
ResultType | eq |
|
deviceId | is_null |
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.
| Field | Source |
|---|---|
AppDisplayName | project |
AppId | project |
AuthenticationRequirement | project |
Browser | project |
City | project |
ClientAppUsed | project |
ConditionalAccessStatus | project |
DeviceId | project |
DeviceName | project |
HomeTenantId | project |
IPAddress | project |
Identity | project |
Location | project |
MfaDetail | project |
OS | project |
ResourceDisplayName | project |
ResourceTenantId | project |
ResultDescription | project |
ResultType | project |
State | project |
Status | project |
TimeGenerated | project |
Type | project |
UserAgent | project |
UserDisplayName | project |
UserId | project |
UserPrincipalName | project |
Name | extend |
UPNSuffix | extend |