Detection rules › Kusto

Microsoft Entra ID UserAgent OS Missmatch

Status
available
Severity
medium
Time window
1d
Group by
DeviceOS, UserAgentOS, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

This query extracts the operating system from the UserAgent header and compares this to the DeviceDetail information present in Microsoft Entra ID.

MITRE ATT&CK coverage

TacticTechniques
Stealth

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 6a638d80-f6b2-473b-9087-3cac78a84b40
name: Microsoft Entra ID UserAgent OS Missmatch
description: |
  This query extracts the operating system from the UserAgent header and compares this to the DeviceDetail information present in Microsoft Entra ID.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1036
query: |
  let timeframe = 1d;
  let ExtractOSFromUA=(ua:string) {
      case(
          ua has "Windows NT 6.0", "Windows Vista/Windows Server 2008",
          ua has "Windows NT 6.1", "Windows 7/Windows Server 2008R2",
          ua has "Windows NT 6.1", "Windows 7/Windows Server 2008",
          ua has "Windows NT 6.2", "Windows 8/Windows Server 2012",
          ua has "Windows NT 6.3", "Windows 8.1/Windows Server 2012R2",
          ua has "Windows NT 10.0", "Windows 10",
          ua has "Windows NT 11.0", "Windows 11",
          ua has "Windows Phone", "WindowsPhone",
          ua has "Android", "Android",
          ua has "iPhone;", "IOS",
          ua has "iPad;", "IOS",
          ua has "Polycom/", "Polycom",
          ua has "Darwin/", "MacOS",
          ua has "Mac OS X", "MacOS",
          ua has "macOS", "MacOS",
          ua has "ubuntu", "Linux",
          ua has "Linux", "Linux",
          ua has "curl", "CLI",
          ua has "python", "CLI",
          "Unknown"
      )
  };
  // Query to obtain 'simplified' user agents in a given timespan.
  union withsource=tbl_name AADNonInteractiveUserSignInLogs, SigninLogs
  | where TimeGenerated >= ago(timeframe)
  | extend UserAgentOS=tolower(ExtractOSFromUA(UserAgent))
  | where not(isempty(UserAgent))
  | where not(isempty(AppId))
  | where ResultType == 0
  | extend DeviceOS=tolower(DeviceDetail_dynamic.operatingSystem)
  | where not(isempty(DeviceOS))
  | where not(UserAgentOS == "unknown")
  // Look for matches both ways, since sometimes the browser OS is more specific and sometimes the DeviceOS is more specific.
  | where not(UserAgentOS contains DeviceOS) and not(DeviceOS contains UserAgentOS)
  | where not(DeviceOS == "ios" and UserAgentOS == "macos") // This can happen for 'request desktop site'
  | where not(DeviceOS == "android" and UserAgentOS == "linux") // Android and Linux sometimes confused
  | summarize count(), arg_min(TimeGenerated,*) by DeviceOS, UserAgentOS, UserPrincipalName
  // Begin allow-list.
  // End allow-list.
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 0: let

let timeframe = 1d;
let ExtractOSFromUA=(ua:string) {
    case(
        ua has "Windows NT 6.0", "Windows Vista/Windows Server 2008",
        ua has "Windows NT 6.1", "Windows 7/Windows Server 2008R2",
        ua has "Windows NT 6.1", "Windows 7/Windows Server 2008",
        ua has "Windows NT 6.2", "Windows 8/Windows Server 2012",
        ua has "Windows NT 6.3", "Windows 8.1/Windows Server 2012R2",
        ua has "Windows NT 10.0", "Windows 10",
        ua has "Windows NT 11.0", "Windows 11",
        ua has "Windows Phone", "WindowsPhone",
        ua has "Android", "Android",
        ua has "iPhone;", "IOS",
        ua has "iPad;", "IOS",
        ua has "Polycom/", "Polycom",
        ua has "Darwin/", "MacOS",
        ua has "Mac OS X", "MacOS",
        ua has "macOS", "MacOS",
        ua has "ubuntu", "Linux",
        ua has "Linux", "Linux",
        ua has "curl", "CLI",
        ua has "python", "CLI",
        "Unknown"
    )
};

Stage 1: union

union of 2 branches

Stage 2: source

AADNonInteractiveUserSignInLogs

Stage 3: source

SigninLogs

Stage 4: where

where TimeGenerated >= ago(86400s)

Stage 5: extend

extend UserAgentOS

Stage 6: where

where not (isempty(UserAgent))

Stage 7: where

where not (isempty(AppId))

Stage 8: where

where ResultType == 0

Stage 9: extend

extend DeviceOS

Stage 10: where

where not (isempty(DeviceOS))

Stage 11: where

where not (UserAgentOS =~ "unknown")

Stage 12: where

where not (DeviceOS contains UserAgentOS) and not (UserAgentOS contains DeviceOS)

Stage 13: where

where not ((DeviceOS =~ "ios" and UserAgentOS =~ "macos"))

Stage 14: where

where not ((DeviceOS =~ "android" and UserAgentOS =~ "linux"))

Stage 15: summarize

summarize by DeviceOS, UserAgentOS, UserPrincipalName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
UserAgentis_null(no value, null check)excludes:UserAgent
AppIdis_null(no value, null check)excludes:AppId
DeviceOSis_null(no value, null check)excludes:DeviceOS
UserAgentOSequnknownexcludes:UserAgentOS field:"UserAgentOS" value:"unknown"
DeviceOScross_field_compareUserAgentOSexcludes:DeviceOS field:"DeviceOS" value:"UserAgentOS"
UserAgentOScross_field_compareDeviceOSexcludes:UserAgentOS field:"UserAgentOS" value:"DeviceOS"
DeviceOSeqiosexcludes:DeviceOS field:"DeviceOS" value:"ios"
UserAgentOSeqmacosexcludes:UserAgentOS field:"UserAgentOS" value:"macos"
DeviceOSeqandroidexcludes:DeviceOS field:"DeviceOS" value:"android"
UserAgentOSeqlinuxexcludes:UserAgentOS field:"UserAgentOS" value:"linux"

Output fields

These fields are emitted when the rule matches.

FieldSource
DeviceOSsummarize
UserAgentOSsummarize
UserPrincipalNamesummarize