Detection rules › Kusto
Hunt domains with Seamless SSO enabled in Entra ID Connect
With below KQL query you can search through the IdentityLogon events of Microsoft Defender for Identity to find users and devices still using Seamless SSO in Entra ID Connect. This feature has been marked by the community multiple times as a security risk, and should be disabled if not in use. The KQL query returns the domains where Seamless SSO is enabled, allong with the related users and devices. On top of that, devices get enriched to find their OS distribution, version, and join type and tells you if Seamless SSO is expected to be used for the related device or not. If there are no results or if all results are showing 'No' for the 'Seamless SSO Expected' column, it should be save to disable the feature in Entra ID connect. !Important: This query relies on the Domain Controller EventID 4769 and Defender for Identity. Make sure the EventID is being logged and Defender for Identity is healthy. For more information see references!
References
- https://nathanmcnulty.com/blog/2025/08/finding-seamless-sso-usage/#
- https://ourcloudnetwork.com/why-you-should-disable-seamless-sso-in-microsoft-entra-connect/
- https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso-faq#how-can-i-disable-seamless-sso-
- https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso
Rule body yaml
// Get all device info we can find
let devices = (
DeviceInfo
// Search for 14 days
| where TimeGenerated > ago(14d)
// Normalize DeviceName
// --> if it is an IP Address we keep it
// --> If it is not an IP Address we only use the hostname for correlation
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
// Only get interesting data
| distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);
IdentityLogonEvents
// Get the last 30 days of logon events on Domain Controllers
| where TimeGenerated > ago(30d)
// Search for Seamless SSO events
| where Application == "Active Directory" and Protocol == "Kerberos"
| where TargetDeviceName == "AZUREADSSOACC"
// Save the domain name of the Domain Controller
| extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1])
// Normalize DeviceName
// --> if it is an IP Address we keep it
// --> If it is not an IP Address we only use the hostname for correlation
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
// Only use interesting data and find more info regarding the source device
| distinct AccountUpn, OnPremisesDomainName, DeviceName
| join kind=leftouter devices on DeviceName
| project-away DeviceName1
// Check if Seamless SSO usage is expected
| extend ['Seamless SSO Expected'] = case(
// Cases where we do not expect Seamless SSO to be used
JoinType == "Hybrid Azure AD Join" or
JoinType == "AAD Joined" or
JoinType == "AAD Registered", "No",
// Cases where we do expect Seamless SSO to be used
JoinType == "Domain Joined" or
(OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes",
// Cases that need to be verified
"Unknown (to verify)"
)
Stages and Predicates
Let binding: devices
let devices = (
DeviceInfo
| where TimeGenerated > ago(14d)
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
| distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);
Stage 1: source
IdentityLogonEvents
Stage 2: where
| where TimeGenerated > ago(30d)
Stage 3: where
| where Application == "Active Directory" and Protocol == "Kerberos"
Stage 4: where
| where TargetDeviceName == "AZUREADSSOACC"
Stage 5: extend
| extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1])
Stage 6: extend
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
DeviceName =ipv4_is_in_range(DeviceName, "10.0.0.0/8")DeviceNametolower(split(DeviceName, ".")[0])Stage 7: distinct
| distinct AccountUpn, OnPremisesDomainName, DeviceName
Stage 8: join
| join kind=leftouter devices on DeviceName
Stage 9: project-away
| project-away DeviceName1
Stage 10: extend
| extend ['Seamless SSO Expected'] = case(
JoinType == "Hybrid Azure AD Join" or
JoinType == "AAD Joined" or
JoinType == "AAD Registered", "No",
JoinType == "Domain Joined" or
(OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes",
"Unknown (to verify)"
)
Seamless SSO Expected =((JoinType == "Hybrid Azure AD Join" or JoinType == "AAD Joined") or JoinType == "AAD Registered")"No"(JoinType == "Domain Joined" or (OSPlatform startswith "Windows" and OSVersion < 10.0))"Yes""Unknown (to verify)"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 |
|---|---|---|
Application | eq |
|
Protocol | eq |
|
TargetDeviceName | 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 |
|---|---|
OnPremisesDomainName | extend |
DeviceName | extend |
Seamless SSO Expected | extend |