Detection rules › Kusto

External user added and removed in short timeframe

Status
available
Severity
low
Time window
1h
Group by
MemberAdded, MemberRemoved
Source
github.com/Azure/Azure-Sentinel

'This detection flags the occurrences of external user accounts that are added to a Team and then removed within one hour.'

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1136 Create Account

Rule body kusto

id: bff093b2-500e-4ae5-bb49-a5b1423cbd5b
name: External user added and removed in short timeframe
description: |
  'This detection flags the occurrences of external user accounts that are added to a Team and then removed within one hour.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (Teams)
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1136
query: |
  let TeamsAddDel = (Op:string){
  OfficeActivity
  | where OfficeWorkload =~ "MicrosoftTeams"
  | where Operation == Op
  | where Members has ("#EXT#")
  | mv-expand Members
  | extend UPN = tostring(Members.UPN)
  | where UPN has ("#EXT#")
  | project TimeGenerated, Operation, UPN, UserId, TeamName, ClientIP
  };
  let TeamsAdd = TeamsAddDel("MemberAdded")
  | project TimeAdded=TimeGenerated, Operation, MemberAdded = UPN, UserWhoAdded = UserId, TeamName, ClientIP;
  let TeamsDel = TeamsAddDel("MemberRemoved")
  | project TimeDeleted=TimeGenerated, Operation, MemberRemoved = UPN, UserWhoDeleted = UserId, TeamName, ClientIP;
  TeamsAdd
  | join kind=inner (TeamsDel) on $left.MemberAdded == $right.MemberRemoved
  | where TimeDeleted > TimeAdded
  | project TimeAdded, TimeDeleted, MemberAdded_Removed = MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName, ClientIP
  | extend MemberAdded_RemovedAccountName = tostring(split(MemberAdded_Removed, "@")[0]), MemberAdded_RemovedAccountUPNSuffix = tostring(split(MemberAdded_Removed, "@")[1])
  | extend UserWhoAddedAccountName = tostring(split(UserWhoAdded, "@")[0]), UserWhoAddedAccountUPNSuffix = tostring(split(UserWhoAdded, "@")[1])
  | extend UserWhoDeletedAccountName = tostring(split(UserWhoDeleted, "@")[0]), UserWhoDeletedAccountUPNSuffix = tostring(split(UserWhoDeleted, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: MemberAdded_Removed
      - identifier: Name
        columnName: MemberAdded_RemovedAccountName
      - identifier: UPNSuffix
        columnName: MemberAdded_RemovedAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserWhoAdded
      - identifier: Name
        columnName: UserWhoAddedAccountName
      - identifier: UPNSuffix
        columnName: UserWhoAddedAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserWhoDeleted
      - identifier: Name
        columnName: UserWhoDeletedAccountName
      - identifier: UPNSuffix
        columnName: UserWhoDeletedAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 2.1.3
kind: Scheduled

Stages and Predicates

Let binding: TeamsAddDel

let TeamsAddDel = (Op:string){
OfficeActivity
| where OfficeWorkload =~ "MicrosoftTeams"
| where Operation == Op
| where Members has ("#EXT#")
| mv-expand Members
| extend UPN = tostring(Members.UPN)
| where UPN has ("#EXT#")
| project TimeGenerated, Operation, UPN, UserId, TeamName, ClientIP
};

Let binding: TeamsDel

let TeamsDel = TeamsAddDel("MemberRemoved")
| project TimeDeleted=TimeGenerated, Operation, MemberRemoved = UPN, UserWhoDeleted = UserId, TeamName, ClientIP;

Derived from TeamsAddDel.

The stages below define let TeamsAdd (the rule's main pipeline source).

Stage 1: source

TeamsAddDel("MemberAdded")

Stage 2: project

| project TimeAdded=TimeGenerated, Operation, MemberAdded = UPN, UserWhoAdded = UserId, TeamName, ClientIP

The stages below run on TeamsAdd (the outer pipeline).

Stage 3: join

TeamsAdd
| join kind=inner (TeamsDel) on $left.MemberAdded == $right.MemberRemoved

Stage 4: where

| where TimeDeleted > TimeAdded

Stage 5: project

| project TimeAdded, TimeDeleted, MemberAdded_Removed = MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName, ClientIP

Stage 6: extend (3 consecutive steps)

| extend MemberAdded_RemovedAccountName = tostring(split(MemberAdded_Removed, "@")[0]), MemberAdded_RemovedAccountUPNSuffix = tostring(split(MemberAdded_Removed, "@")[1])
| extend UserWhoAddedAccountName = tostring(split(UserWhoAdded, "@")[0]), UserWhoAddedAccountUPNSuffix = tostring(split(UserWhoAdded, "@")[1])
| extend UserWhoDeletedAccountName = tostring(split(UserWhoDeleted, "@")[0]), UserWhoDeletedAccountUPNSuffix = tostring(split(UserWhoDeleted, "@")[1])

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
TimeDeletedgt
  • TimeAdded 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
ClientIPproject
MemberAdded_Removedproject
TeamNameproject
TimeAddedproject
TimeDeletedproject
UserWhoAddedproject
UserWhoDeletedproject
MemberAdded_RemovedAccountNameextend
MemberAdded_RemovedAccountUPNSuffixextend
UserWhoAddedAccountNameextend
UserWhoAddedAccountUPNSuffixextend
UserWhoDeletedAccountNameextend
UserWhoDeletedAccountUPNSuffixextend