Detection rules › Kusto
Guest accounts added in Entra ID Groups other than the ones specified
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
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
| Persistence | T1078.004 Valid Accounts: Cloud Accounts, T1136.003 Create Account: Cloud Account |
| Discovery | T1087.004 Account Discovery: Cloud Account |
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.
| Field | Kind | Excluded values |
|---|---|---|
AADGroupId | in | Custom 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.
| Field | Kind | Values |
|---|---|---|
InvitedUserPrincipalName | match |
|
OperationName | in |
|
displayName | eq |
|
type | eq |
|
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.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
Name | extend |
UPNSuffix | extend |
InvitedUPNSuffix | extend |
InvitedUserName | extend |