Detection rules › Kusto

Anomalous Single Factor Signin

Severity
low
Time window
7d
Author
Pete Bryan
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
Initial AccessT1078.004 Valid Accounts: Cloud Accounts

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

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.

FieldKindExcluded values
AutonomousSystemNumbereqknown_asn
LocationDetaileqknown_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.

FieldKindValues
AuthenticationRequirementeq
  • singleFactorAuthentication transforms: cased
ResultTypeeq
  • 0 transforms: cased
deviceIdis_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
AppDisplayNameproject
AppIdproject
AuthenticationRequirementproject
Browserproject
Cityproject
ClientAppUsedproject
ConditionalAccessStatusproject
DeviceIdproject
DeviceNameproject
HomeTenantIdproject
IPAddressproject
Identityproject
Locationproject
MfaDetailproject
OSproject
ResourceDisplayNameproject
ResourceTenantIdproject
ResultDescriptionproject
ResultTypeproject
Stateproject
Statusproject
TimeGeneratedproject
Typeproject
UserAgentproject
UserDisplayNameproject
UserIdproject
UserPrincipalNameproject
Nameextend
UPNSuffixextend