Detection rules › Kusto

Guest accounts added in Entra ID Groups other than the ones specified

Status
available
Severity
high
Time window
2h
Source
github.com/Azure/Azure-Sentinel

Guest Accounts are added in the Organization Tenants to perform various tasks i.e projects execution, support etc.. This detection notifies when guest users are added to Microsoft Entra ID Groups other than the ones specified and poses a risk to gain access to sensitive apps or data.

MITRE ATT&CK coverage

Event coverage

Rule body kusto

id: 6ab1f7b2-61b8-442f-bc81-96afe7ad8c53
name: Guest accounts added in Entra ID Groups other than the ones specified
description: |
  'Guest Accounts are added in the Organization Tenants to perform various tasks i.e projects execution, support etc.. This detection notifies when guest users are added to Microsoft Entra ID Groups other than the ones specified and poses a risk to gain access to sensitive apps or data.'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Persistence
  - Discovery
relevantTechniques:
  - T1078.004
  - T1136.003
  - T1087.004
query: |
  // OBJECT ID of AAD Groups can be found by navigating to Azure Active Directory then from menu on the left, select Groups and from the list shown of AAD Groups, the Second Column shows the ObjectID of each
  let GroupIDs = dynamic(["List with Custom AAD GROUP OBJECT ID 1","Custom AAD GROUP OBJECT ID 2"]);
  AuditLogs
  | where OperationName in ('Add member to group', 'Add owner to group')
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  // Uncomment the following line to filter events where the inviting user was a guest user
  //| where InitiatedBy has_any ("CUSTOM DOMAIN NAME#", "#EXT#")
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend InvitedUserPrincipalName = trim(@'"',tostring(TargetResource.userPrincipalName)),
                 Properties = TargetResource.modifiedProperties
    )
  | mv-apply Property = Properties on 
    (
        where Property.displayName =~ "Group.DisplayName"
        | extend AADGroup = trim('"',tostring(Property.newValue))
    )
  | where InvitedUserPrincipalName has_any ("CUSTOM DOMAIN NAME#", "#EXT#")
  | mv-apply Property = Properties on
     (
       where Property.displayName =~ "Group.ObjectID"
       | extend AADGroupId = trim('"',tostring(Property.newValue))
     )
  | project-away TargetResource, Property
  | where AADGroupId !in (GroupIDs)
  | extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
  | extend InvitedUserName = tostring(split(InvitedUserPrincipalName,'@',0)[0]), InvitedUPNSuffix = tostring(split(InvitedUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InvitedUserPrincipalName
      - identifier: Name
        columnName: InvitedUserName
      - identifier: UPNSuffix
        columnName: InvitedUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: AADGroup
      - identifier: ObjectGuid
        columnName: AADGroupId
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let GroupIDs = dynamic(["List with Custom AAD GROUP OBJECT ID 1","Custom AAD GROUP OBJECT ID 2"]);

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName in ('Add member to group', 'Add owner to group')

Stage 3: extend (5 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))

Stage 4: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend InvitedUserPrincipalName = trim(@'"',tostring(TargetResource.userPrincipalName)),
               Properties = TargetResource.modifiedProperties
  )

Stage 5: kusto:mv-apply

| mv-apply Property = Properties on 
  (
      where Property.displayName =~ "Group.DisplayName"
      | extend AADGroup = trim('"',tostring(Property.newValue))
  )

Stage 6: where

| where InvitedUserPrincipalName has_any ("CUSTOM DOMAIN NAME#", "#EXT#")

Stage 7: kusto:mv-apply

| mv-apply Property = Properties on
   (
     where Property.displayName =~ "Group.ObjectID"
     | extend AADGroupId = trim('"',tostring(Property.newValue))
   )

Stage 8: project-away

| project-away TargetResource, Property

Stage 9: where

| where AADGroupId !in (GroupIDs)

Stage 10: extend

| extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Stage 11: extend

| extend InvitedUserName = tostring(split(InvitedUserPrincipalName,'@',0)[0]), InvitedUPNSuffix = tostring(split(InvitedUserPrincipalName,'@',1)[0])

Exclusions

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

FieldKindExcluded values
AADGroupIdinCustom AAD GROUP OBJECT ID 2, List with Custom AAD GROUP OBJECT ID 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
InvitedUserPrincipalNamematch
  • #EXT#
  • CUSTOM DOMAIN NAME#
OperationNamein
  • Add member to group transforms: cased
  • Add owner to group transforms: cased
displayNameeq
  • Group.DisplayName
  • Group.ObjectID
typeeq
  • User

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
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
Nameextend
UPNSuffixextend
InvitedUPNSuffixextend
InvitedUserNameextend