Detection rules › Kusto
Cross-tenant Access Settings Organization Added
Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when an Organization is added other than the list that is supposed to exist from the Microsoft Entra ID Cross-tenant Access Settings.
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
| Provider | Event | Title |
|---|---|---|
| Entra-AuditLogs | _catch_all | Entra ID audit event (any operation) |
Rule body kusto
id: 757e6a79-6d23-4ae6-9845-4dac170656b5
name: Cross-tenant Access Settings Organization Added
description: |
'Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when an Organization is added other than the list that is supposed to exist from the Microsoft Entra ID Cross-tenant Access Settings.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Persistence
- Discovery
relevantTechniques:
- T1078.004
- T1136.003
- T1087.004
query: |
// Tenants IDs can be found by navigating to Azure Active Directory then from menu on the left, select External Identities, then from menu on the left, select Cross-tenant access settings and from the list shown of Tenants
let ExpectedTenantIDs = dynamic(["List of expected tenant IDs","Tenant ID 2"]);
AuditLogs
| where OperationName has "Add a partner to cross-tenant access setting"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Policy"
| extend Properties = TargetResource.modifiedProperties
)
| mv-apply Property = Properties on
(
where Property.displayName =~ "tenantId"
| extend ExtTenantIDAdded = trim('"',tostring(Property.newValue))
)
| where ExtTenantIDAdded !in (ExpectedTenantIDs)
| 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))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatingAppName
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled
Stages and Predicates
Parameters
let ExpectedTenantIDs = dynamic(["List of expected tenant IDs","Tenant ID 2"]);
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName has "Add a partner to cross-tenant access setting"
Stage 3: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Policy"
| extend Properties = TargetResource.modifiedProperties
)
Stage 4: kusto:mv-apply
| mv-apply Property = Properties on
(
where Property.displayName =~ "tenantId"
| extend ExtTenantIDAdded = trim('"',tostring(Property.newValue))
)
Stage 5: where
| where ExtTenantIDAdded !in (ExpectedTenantIDs)
Stage 6: extend (6 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))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
ExtTenantIDAdded | in | List of expected tenant IDs, Tenant ID 2 |
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 |
|---|---|---|
OperationName | match |
|
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 |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |