Detection rules › Kusto

Detect presence of uncommon user agents in web requests (ASIM Web Session)

Status
available
Severity
medium
Time window
14d
Group by
DstIpAddr, DstPortNumber, HttpUserAgent, SrcHostname, SrcIpAddr, SrcUsername, Url
Source
github.com/Azure/Azure-Sentinel

'This rule assists in detecting rare user agents, which may indicate web browsing activity by an unconventional process different from the usual ones. The rule specifically searches for UserAgent strings that have not been seen in the past 14 days. This query will perform better when run over summarized data'

MITRE ATT&CK coverage

Rule body kusto

id: 2d50d937-d7f2-4c05-b151-9af7f9ec747e
name: Detect presence of uncommon user agents in web requests (ASIM Web Session)
description: |
  'This rule assists in detecting rare user agents, which may indicate web browsing activity by an unconventional process different from the usual ones. The rule specifically searches for UserAgent strings that have not been seen in the past 14 days. This query will perform better when run over summarized data'
severity: Medium
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let lookBack = 14d;
  let timeframe = 1h;
  // calculate avg. eps(events per second)
  let eps = materialize(_Im_WebSession(starttime=ago(1d))
      | project TimeGenerated
      | summarize AvgPerSec = count() / 3600 by bin(TimeGenerated, 1h)
      | summarize round(avg(AvgPerSec))
      );
  let summarizationexist  = (
      union isfuzzy=true 
          (
          WebSession_Summarized_SrcInfo_CL
          | where EventTime_t > ago(1d) 
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend sumexist = (maxv > 1)
      );
  let allUserAgents = union isfuzzy=true
          (
          (datatable(exists: int, sumexist: bool)[1, false]
          | where toscalar(eps) > 1000
          | join (summarizationexist) on sumexist)
          | join (
              _Im_WebSession(starttime=todatetime(ago(2d)), endtime=bin(ago(timeframe), 1h), eventresult='Success')
              | where isnotempty(HttpUserAgent)
              | summarize UserAgentsSet =  make_set(HttpUserAgent) // This could contain upto 10,48,576 unique values
              | extend exists=int(1)
              )
              on exists
          | project-away exist*, maxv, sum*
          ),
          (
          (datatable(exists: int, sumexist: bool)[1, false]
          | where toscalar(eps) between (501 .. 1000)
          | join (summarizationexist) on sumexist)
          | join (
              _Im_WebSession(starttime=todatetime(ago(3d)), endtime=bin(ago(timeframe), 1h), eventresult='Success')
              | where isnotempty(HttpUserAgent)
              | summarize UserAgentsSet = make_set(HttpUserAgent) // This could contain upto 10,48,576 unique values
              | extend exists=int(1)
              )
              on exists
          | project-away exist*, maxv, sum*
          ),
          (
          (datatable(exists: int, sumexist: bool)[1, false]
          | where toscalar(eps) <= 500
          | join (summarizationexist) on sumexist)
          | join (
              _Im_WebSession(starttime=todatetime(ago(4d)), endtime=bin(ago(timeframe), 1h), eventresult='Success')
              | where isnotempty(HttpUserAgent)
              | summarize UserAgentsSet = make_set(HttpUserAgent) // This could contain upto 10,48,576 unique values
              | extend exists=int(1)
              )
              on exists
          | project-away exist*, maxv, sum*
          ),
          (
          WebSession_Summarized_SrcInfo_CL
          | where EventTime_t between (ago(14d) .. ago(timeframe))
              and isnotempty(HttpUserAgent_s)
              and EventResult_s =~ 'Success'
          | summarize UserAgentsSet = make_set(HttpUserAgent_s) // This could contain upto 10,48,576 unique values
          );
  _Im_WebSession (starttime=bin(ago(timeframe), 1h), eventresult='Success')
  | project
      SrcIpAddr,
      SrcUsername,
      SrcHostname,
      DstIpAddr,
      DstPortNumber,
      Url,
      HttpUserAgent,
      TimeGenerated
  | where isnotempty(HttpUserAgent) and HttpUserAgent !in~ (allUserAgents)
  | summarize
      EventCount=count(),
      EventStartTime=min(TimeGenerated),
      EventEndTime=max(TimeGenerated)
      by
      SrcIpAddr,
      SrcUsername,
      SrcHostname,
      DstIpAddr,
      Url,
      HttpUserAgent,
      DstPortNumber
  | extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DstIpAddr
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventCount: EventCount
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
  HttpUserAgent: HttpUserAgent
  DstPortNumber: DstPortNumber
alertDetailsOverride:
  alertDisplayNameFormat: "User '{{SrcUsername}}' with IP '{{SrcIpAddr}}' has been observed accessing URL '{{Url}}' using a rare user agent."
  alertDescriptionFormat: "The user agent '{{HttpUserAgent}}' has not been observed in the past 14 days. Conduct research on the user agent string to determine if it is associated with a known legitimate bot or if it is potentially linked to malicious activity. The URL is associated with the IP address '{{DstIpAddr}}'."
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let lookBack = 14d;
let timeframe = 1h;

Let binding: eps

let eps = materialize(_Im_WebSession(starttime=ago(1d))
    | project TimeGenerated
    | summarize AvgPerSec = count() / 3600 by bin(TimeGenerated, 1h)
    | summarize round(avg(AvgPerSec))
    );

Let binding: summarizationexist

let summarizationexist = (
    union isfuzzy=true 
        (
        WebSession_Summarized_SrcInfo_CL
        | where EventTime_t > ago(1d) 
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend sumexist = (maxv > 1)
    );

Let binding: allUserAgents

let allUserAgents = union isfuzzy=true
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | where toscalar(eps) > 1000
        | join (summarizationexist) on sumexist)
        | join (
            _Im_WebSession(starttime=todatetime(ago(2d)), endtime=bin(ago(timeframe), 1h), eventresult='Success')
            | where isnotempty(HttpUserAgent)
            | summarize UserAgentsSet =  make_set(HttpUserAgent)
            | extend exists=int(1)
            )
            on exists
        | project-away exist*, maxv, sum*
        ),
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | where toscalar(eps) between (501 .. 1000)
        | join (summarizationexist) on sumexist)
        | join (
            _Im_WebSession(starttime=todatetime(ago(3d)), endtime=bin(ago(timeframe), 1h), eventresult='Success')
            | where isnotempty(HttpUserAgent)
            | summarize UserAgentsSet = make_set(HttpUserAgent)
            | extend exists=int(1)
            )
            on exists
        | project-away exist*, maxv, sum*
        ),
        (
        (datatable(exists: int, sumexist: bool)[1, false]
        | where toscalar(eps) <= 500
        | join (summarizationexist) on sumexist)
        | join (
            _Im_WebSession(starttime=todatetime(ago(4d)), endtime=bin(ago(timeframe), 1h), eventresult='Success')
            | where isnotempty(HttpUserAgent)
            | summarize UserAgentsSet = make_set(HttpUserAgent)
            | extend exists=int(1)
            )
            on exists
        | project-away exist*, maxv, sum*
        ),
        (
        WebSession_Summarized_SrcInfo_CL
        | where EventTime_t between (ago(14d) .. ago(timeframe))
            and isnotempty(HttpUserAgent_s)
            and EventResult_s =~ 'Success'
        | summarize UserAgentsSet = make_set(HttpUserAgent_s)
        );

Derived from timeframe, eps, summarizationexist.

Stage 1: source

_Im_WebSession (starttime=bin(ago(timeframe), 1h), eventresult='Success')

Stage 2: project

| project
    SrcIpAddr,
    SrcUsername,
    SrcHostname,
    DstIpAddr,
    DstPortNumber,
    Url,
    HttpUserAgent,
    TimeGenerated

Stage 3: where

| where isnotempty(HttpUserAgent) and HttpUserAgent !in~ (allUserAgents)

References allUserAgents (defined above).

Stage 4: summarize

| summarize
    EventCount=count(),
    EventStartTime=min(TimeGenerated),
    EventEndTime=max(TimeGenerated)
    by
    SrcIpAddr,
    SrcUsername,
    SrcHostname,
    DstIpAddr,
    Url,
    HttpUserAgent,
    DstPortNumber

Stage 5: extend

| extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
HttpUserAgenteqallUserAgents

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
HttpUserAgentis_not_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
DstIpAddrsummarize
DstPortNumbersummarize
EventCountsummarize
EventEndTimesummarize
EventStartTimesummarize
HttpUserAgentsummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
Urlsummarize
Nameextend
UPNSuffixextend