Detection rules › Kusto

Microsoft Entra ID Rare UserAgent App Sign-in

Status
available
Severity
medium
Time window
7d
Group by
AppDisplayName, AppId, SimpleUserAgent, UserAgent, UserAgentType, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

This query establishes a baseline of the type of UserAgent (i.e. browser, office application, etc) that is typically used for a particular application by looking back for a number of days. It then searches the current day for any deviations from this pattern, i.e. types of UserAgents not seen before in combination with this application.

MITRE ATT&CK coverage

TacticTechniques
Stealth

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 87d5cd18-211d-4fd4-9b86-65d23fed87ea
name: Microsoft Entra ID Rare UserAgent App Sign-in
description: |
  This query establishes a baseline of the type of UserAgent (i.e. browser, office application, etc) that is typically used for a particular application by looking back for a number of days. 
  It then searches the current day for any deviations from this pattern, i.e. types of UserAgents not seen before in combination with this application.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1036
query: |
  let minimumAppThreshold = 100;
  let timeframe = 1d;
  let lookback_timeframe= 7d;
  let ExtractBrowserTypeFromUA=(ua:string) {
      // Note: these are in a specific order since, for example, Edge contains "Chrome/" and "Edge/" strings.
      case(
          ua has "Edge/", dynamic({"AgentType": "Browser", "AgentName": "Edge"}),
          ua has "Edg/", dynamic({"AgentType": "Browser", "AgentName": "Edge"}),
          ua has "Trident/", dynamic({"AgentType": "Browser", "AgentName": "Internet Explorer"}),
          ua has "Chrome/" and ua has "Safari/", dynamic({"AgentType": "Browser", "AgentName": "Chrome"}),
          ua has "Gecko/" and ua has "Firefox/", dynamic({"AgentType": "Browser", "AgentName": "Firefox"}),
          not(ua has "Mobile/") and ua has "Safari/" and ua has "Version/", dynamic({"AgentType": "Browser", "AgentName": "Safari"}),
          ua startswith "Dalvik/" and ua has "Android", dynamic({"AgentType": "Browser", "AgentName": "Android Browser"}),
          ua startswith "MobileSafari//", dynamic({"AgentType": "Browser", "AgentName": "Mobile Safari"}),
          ua has "Mobile/" and ua has "Safari/" and ua has "Version/", dynamic({"AgentType": "Browser", "AgentName": "Mobile Safari"}),
          ua has "Mobile/" and ua has "FxiOS/", dynamic({"AgentType": "Browser", "AgentName": "IOS Firefox"}),
          ua has "Mobile/" and ua has "CriOS/", dynamic({"AgentType": "Browser", "AgentName": "IOS Chrome"}),
          ua has "Mobile/" and ua has "WebKit/", dynamic({"AgentType": "Browser", "AgentName": "Mobile Webkit"}),
          //
          ua startswith "Excel/", dynamic({"AgentType": "OfficeApp", "AgentName": "Excel"}),
          ua startswith "Outlook/", dynamic({"AgentType": "OfficeApp", "AgentName": "Outlook"}),
          ua startswith "OneDrive/", dynamic({"AgentType": "OfficeApp", "AgentName": "OneDrive"}),
          ua startswith "OneNote/", dynamic({"AgentType": "OfficeApp", "AgentName": "OneNote"}),
          ua startswith "Office/", dynamic({"AgentType": "OfficeApp", "AgentName": "Office"}),
          ua startswith "PowerPoint/", dynamic({"AgentType": "OfficeApp", "AgentName": "PowerPoint"}),
          ua startswith "PowerApps/", dynamic({"AgentType": "OfficeApp", "AgentName": "PowerApps"}),
          ua startswith "SharePoint/", dynamic({"AgentType": "OfficeApp", "AgentName": "SharePoint"}),
          ua startswith "Word/", dynamic({"AgentType": "OfficeApp", "AgentName": "Word"}),
          ua startswith "Visio/", dynamic({"AgentType": "OfficeApp", "AgentName": "Visio"}),
          ua startswith "Whiteboard/", dynamic({"AgentType": "OfficeApp", "AgentName": "Whiteboard"}),
          ua =~ "Mozilla/5.0 (compatible; MSAL 1.0)", dynamic({"AgentType": "OfficeApp", "AgentName": "Office Telemetry"}),
          //
          ua has ".NET CLR", dynamic({"AgentType": "Custom", "AgentName": "Dotnet"}),
          ua startswith "Java/", dynamic({"AgentType": "Custom", "AgentName": "Java"}),
          ua startswith "okhttp/", dynamic({"AgentType": "Custom", "AgentName": "okhttp"}),
          ua has "Drupal/", dynamic({"AgentType": "Custom", "AgentName": "Drupal"}),
          ua has "PHP/", dynamic({"AgentType": "Custom", "AgentName": "PHP"}),
          ua startswith "curl/", dynamic({"AgentType": "Custom", "AgentName": "curl"}),
          ua has "python-requests", dynamic({"AgentType": "Custom", "AgentName": "Python"}),
          pack("AgentType","Other","AgentName", extract(@"^([^/]*)/",1,ua))
      )
  };
  // Query to obtain 'simplified' user agents in a given timespan.
  let QueryUserAgents = (start_time:timespan, end_time:timespan) {
      union withsource=tbl_name AADNonInteractiveUserSignInLogs, SigninLogs
      | where TimeGenerated >= ago(start_time)
      | where TimeGenerated < ago(end_time)
      | where ResultType == 0  // Only look at succesful logins
      | extend ParsedUserAgent=ExtractBrowserTypeFromUA(UserAgent)
      | extend UserAgentType=tostring(ParsedUserAgent.AgentType)
      | extend UserAgentName=tostring(ParsedUserAgent.AgentName)
      //| extend SimpleUserAgent=strcat(UserAgentType,"_",UserAgentName)
      | extend SimpleUserAgent=UserAgentType
      | where not(isempty(UserAgent))
      | where not(isempty(AppId))
  };
  // Get baseline usage per application.
  let BaselineUserAgents=materialize(
      QueryUserAgents(lookback_timeframe+timeframe, timeframe)
      | summarize RequestCount=count() by AppId, AppDisplayName, SimpleUserAgent
  );
  let BaselineSummarizedAgents=(
      BaselineUserAgents
      | summarize BaselineUAs=make_set(SimpleUserAgent),BaselineRequestCount=sum(RequestCount) by AppId, AppDisplayName
  );
  QueryUserAgents(timeframe, 0d)
  | summarize count() by AppId, AppDisplayName, UserAgent, SimpleUserAgent
  | join kind=leftanti BaselineUserAgents on AppId, AppDisplayName, SimpleUserAgent
  | join BaselineSummarizedAgents on AppId, AppDisplayName
  | where BaselineRequestCount > minimumAppThreshold // Search only for actively used applications.
  // Get back full original requests.
  | join QueryUserAgents(timeframe, 0d) on AppId, UserAgent
  | project-away ParsedUserAgent, UserAgentName
  | project-reorder TimeGenerated, AppDisplayName, UserPrincipalName, UserAgent, BaselineUAs
  // Begin allow-list.
  // End allow-list.
  | summarize count() by UserPrincipalName, AppDisplayName, AppId, UserAgentType, SimpleUserAgent, UserAgent
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let minimumAppThreshold = 100;
let timeframe = 1d;
let lookback_timeframe = 7d;

let QueryUserAgents is inlined into the numbered stages below.

Let binding: ExtractBrowserTypeFromUA used in Stage 7

let ExtractBrowserTypeFromUA = (ua:string) {
    case(
        ua has "Edge/", dynamic({"AgentType": "Browser", "AgentName": "Edge"}),
        ua has "Edg/", dynamic({"AgentType": "Browser", "AgentName": "Edge"}),
        ua has "Trident/", dynamic({"AgentType": "Browser", "AgentName": "Internet Explorer"}),
        ua has "Chrome/" and ua has "Safari/", dynamic({"AgentType": "Browser", "AgentName": "Chrome"}),
        ua has "Gecko/" and ua has "Firefox/", dynamic({"AgentType": "Browser", "AgentName": "Firefox"}),
        not(ua has "Mobile/") and ua has "Safari/" and ua has "Version/", dynamic({"AgentType": "Browser", "AgentName": "Safari"}),
        ua startswith "Dalvik/" and ua has "Android", dynamic({"AgentType": "Browser", "AgentName": "Android Browser"}),
        ua startswith "MobileSafari//", dynamic({"AgentType": "Browser", "AgentName": "Mobile Safari"}),
        ua has "Mobile/" and ua has "Safari/" and ua has "Version/", dynamic({"AgentType": "Browser", "AgentName": "Mobile Safari"}),
        ua has "Mobile/" and ua has "FxiOS/", dynamic({"AgentType": "Browser", "AgentName": "IOS Firefox"}),
        ua has "Mobile/" and ua has "CriOS/", dynamic({"AgentType": "Browser", "AgentName": "IOS Chrome"}),
        ua has "Mobile/" and ua has "WebKit/", dynamic({"AgentType": "Browser", "AgentName": "Mobile Webkit"}),
        ua startswith "Excel/", dynamic({"AgentType": "OfficeApp", "AgentName": "Excel"}),
        ua startswith "Outlook/", dynamic({"AgentType": "OfficeApp", "AgentName": "Outlook"}),
        ua startswith "OneDrive/", dynamic({"AgentType": "OfficeApp", "AgentName": "OneDrive"}),
        ua startswith "OneNote/", dynamic({"AgentType": "OfficeApp", "AgentName": "OneNote"}),
        ua startswith "Office/", dynamic({"AgentType": "OfficeApp", "AgentName": "Office"}),
        ua startswith "PowerPoint/", dynamic({"AgentType": "OfficeApp", "AgentName": "PowerPoint"}),
        ua startswith "PowerApps/", dynamic({"AgentType": "OfficeApp", "AgentName": "PowerApps"}),
        ua startswith "SharePoint/", dynamic({"AgentType": "OfficeApp", "AgentName": "SharePoint"}),
        ua startswith "Word/", dynamic({"AgentType": "OfficeApp", "AgentName": "Word"}),
        ua startswith "Visio/", dynamic({"AgentType": "OfficeApp", "AgentName": "Visio"}),
        ua startswith "Whiteboard/", dynamic({"AgentType": "OfficeApp", "AgentName": "Whiteboard"}),
        ua =~ "Mozilla/5.0 (compatible; MSAL 1.0)", dynamic({"AgentType": "OfficeApp", "AgentName": "Office Telemetry"}),
        ua has ".NET CLR", dynamic({"AgentType": "Custom", "AgentName": "Dotnet"}),
        ua startswith "Java/", dynamic({"AgentType": "Custom", "AgentName": "Java"}),
        ua startswith "okhttp/", dynamic({"AgentType": "Custom", "AgentName": "okhttp"}),
        ua has "Drupal/", dynamic({"AgentType": "Custom", "AgentName": "Drupal"}),
        ua has "PHP/", dynamic({"AgentType": "Custom", "AgentName": "PHP"}),
        ua startswith "curl/", dynamic({"AgentType": "Custom", "AgentName": "curl"}),
        ua has "python-requests", dynamic({"AgentType": "Custom", "AgentName": "Python"}),
        pack("AgentType","Other","AgentName", extract(@"^([^/]*)/",1,ua))
    )
};

Let binding: BaselineUserAgents used in Stage 11

let BaselineUserAgents = materialize(
    QueryUserAgents(lookback_timeframe+timeframe, timeframe)
    | summarize RequestCount=count() by AppId, AppDisplayName, SimpleUserAgent
);

Let binding: BaselineSummarizedAgents used in Stage 12

let BaselineSummarizedAgents = (
    BaselineUserAgents
    | summarize BaselineUAs=make_set(SimpleUserAgent),BaselineRequestCount=sum(RequestCount) by AppId, AppDisplayName
);

Stages 1 to 9 define let QueryUserAgents (the rule's main pipeline source); stages 10 to 17 run on it.

Stage 1: union

union withsource=tbl_name AADNonInteractiveUserSignInLogs, SigninLogs

Stage 2: source

AADNonInteractiveUserSignInLogs

Stage 3: source

SigninLogs

Stage 4: where

| where TimeGenerated >= ago(start_time)

Stage 5: where

| where TimeGenerated < ago(end_time)

Stage 6: where

| where ResultType == 0

Stage 7: extend (4 consecutive steps)

| extend ParsedUserAgent=ExtractBrowserTypeFromUA(UserAgent)
| extend UserAgentType=tostring(ParsedUserAgent.AgentType)
| extend UserAgentName=tostring(ParsedUserAgent.AgentName)
| extend SimpleUserAgent=UserAgentType

Stage 8: where

| where not(isempty(UserAgent))

Stage 9: where

| where not(isempty(AppId))

Stage 10: summarize

QueryUserAgents
| summarize count() by AppId, AppDisplayName, UserAgent, SimpleUserAgent

Stage 11: join (negated)

| join kind=leftanti BaselineUserAgents on AppId, AppDisplayName, SimpleUserAgent

Stage 12: join

| join BaselineSummarizedAgents on AppId, AppDisplayName

Stage 13: where

| where BaselineRequestCount > minimumAppThreshold

Stage 14: join

| join QueryUserAgents(timeframe, 0d) on AppId, UserAgent

Stage 15: project-away

| project-away ParsedUserAgent, UserAgentName

Stage 16: project-reorder

| project-reorder TimeGenerated, AppDisplayName, UserPrincipalName, UserAgent, BaselineUAs

Stage 17: summarize

| summarize count() by UserPrincipalName, AppDisplayName, AppId, UserAgentType, SimpleUserAgent, UserAgent

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

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNamesummarize
AppIdsummarize
SimpleUserAgentsummarize
UserAgentsummarize
UserAgentTypesummarize
UserPrincipalNamesummarize