Detection rules › Kusto

User joining Zoom meeting from suspicious timezone

Severity
low
Time window
14d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'The alert shows users that join a Zoom meeting from a time zone other than the one the meeting was created in. You can also whitelist known good time zones in the tz_whitelist value using the tz database name format https://en.wikipedia.org/wiki/List_of_tz_database_time_zones'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
Privilege EscalationT1078 Valid Accounts

Rule body kusto

id: 58fc0170-0877-4ea8-a9ff-d805e361cfae
name: User joining Zoom meeting from suspicious timezone
description: |
  'The alert shows users that join a Zoom meeting from a time zone other than the one the meeting was created in.
  You can also whitelist known good time zones in the tz_whitelist value using the tz database name format https://en.wikipedia.org/wiki/List_of_tz_database_time_zones'
severity: Low
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |
  let schedule_lookback = 14d;
  let join_lookback = 1d;
  // If you want to whitelist specific timezones include them in a list here
  let tz_whitelist = dynamic([]);
  let meetings = (
  ZoomLogs
  | where TimeGenerated >= ago(schedule_lookback)
  | where Event =~ "meeting.created"
  | extend MeetingId = tostring(parse_json(MeetingEvents).MeetingId)
  | extend SchedTimezone = tostring(parse_json(MeetingEvents).Timezone));
  ZoomLogs
  | where TimeGenerated >= ago(join_lookback)
  | where Event =~ "meeting.participant_joined"
  | extend JoinedTimeZone = tostring(parse_json(MeetingEvents).Timezone)
  | extend MeetingName = tostring(parse_json(MeetingEvents).MeetingName)
  | extend MeetingId = tostring(parse_json(MeetingEvents).MeetingId)
  | where JoinedTimeZone !in (tz_whitelist)
  | join (meetings) on MeetingId
  | where SchedTimezone != JoinedTimeZone
  | project TimeGenerated, MeetingName, JoiningUser=payload_object_participant_user_name_s, JoinedTimeZone, SchedTimezone, MeetingScheduler=User1
  | extend AccountName = tostring(split(JoiningUser, "@")[0]), AccountUPNSuffix = tostring(split(JoiningUser, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: JoiningUser
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let schedule_lookback = 14d;
let join_lookback = 1d;
let tz_whitelist = dynamic([]);

Let binding: meetings

let meetings = (
ZoomLogs
| where TimeGenerated >= ago(schedule_lookback)
| where Event =~ "meeting.created"
| extend MeetingId = tostring(parse_json(MeetingEvents).MeetingId)
| extend SchedTimezone = tostring(parse_json(MeetingEvents).Timezone));

Derived from schedule_lookback.

Stage 1: source

ZoomLogs

Stage 2: where

| where TimeGenerated >= ago(join_lookback)

Stage 3: where

| where Event =~ "meeting.participant_joined"

Stage 4: extend (3 consecutive steps)

| extend JoinedTimeZone = tostring(parse_json(MeetingEvents).Timezone)
| extend MeetingName = tostring(parse_json(MeetingEvents).MeetingName)
| extend MeetingId = tostring(parse_json(MeetingEvents).MeetingId)

Stage 5: where

| where JoinedTimeZone !in (tz_whitelist)

Stage 6: join

| join (meetings) on MeetingId

Stage 7: where

| where SchedTimezone != JoinedTimeZone

Stage 8: project

| project TimeGenerated, MeetingName, JoiningUser=payload_object_participant_user_name_s, JoinedTimeZone, SchedTimezone, MeetingScheduler=User1

Stage 9: extend

| extend AccountName = tostring(split(JoiningUser, "@")[0]), AccountUPNSuffix = tostring(split(JoiningUser, "@")[1])

Exclusions

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

FieldKindExcluded values
JoinedTimeZoneeq[]

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
Eventeq
  • meeting.created
  • meeting.participant_joined
SchedTimezonene
  • JoinedTimeZone transforms: cased

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
JoinedTimeZoneproject
JoiningUserproject
MeetingNameproject
MeetingSchedulerproject
SchedTimezoneproject
TimeGeneratedproject
AccountNameextend
AccountUPNSuffixextend