Query Details

Analytics Unexpected Entra ID Device

Query

// This query can help you to detect unexpected Entra ID devices.
//
// Click "Save as function", in Parameters write in the fields:
// "timespan" "query_frequency" "14d"
// "timespan" "query_period"    "14d"
// "timespan" "query_wait"      "1h"
//
// If you name the function "UnexpectedEntraIDDevice", you can check the function with queries like the following:
//
// UnexpectedEntraIDDevice()
//
// UnexpectedEntraIDDevice(1h, 14d, 1h)
//
// UnexpectedEntraIDDevice(14d, 14d, 1h)
//
// let query_frequency = 14d;
// let query_period = 14d;
// let query_wait = 1h;
//let Function = (query_frequency:timespan = 14d, query_period:timespan = 14d){
// TrustType possible values: Workplace (indicates bring your own personal devices), AzureAd (Cloud only joined devices), ServerAd (on-premises domain joined devices joined to Azure AD)
// let _ExpectedTrustTypes =
//     _GetWatchlist("Activity-ExpectedSignificantActivity")
//     | where Activity == "EntraIDDeviceTrustType"
//     | mv-expand DeviceOSType = split(tostring(DeviceTrustType = split(Auxiliar, " - ")[1]), ", ") to typeof(string)
//     | project DeviceTrustType = tostring(split(Auxiliar, " - ")[0]), DeviceOSType
// ;
// A device management app could be Microsoft Intune
let _ExpectedDeviceManagementIds = toscalar(
    _GetWatchlist("Activity-ExpectedSignificantActivity")
    | where Activity == "DeviceManagement"
    | summarize make_list(ActorId)
);
// Group Tags in Microsoft Intune
let _ExpectedIntuneOrderIds = toscalar(
    _GetWatchlist("Activity-ExpectedSignificantActivity")
    | where Activity == "IntuneOrderId"
    | summarize make_list(Auxiliar)
);
let _EntraConnectSyncAccounts = toscalar(
    _GetWatchlist("Activity-ExpectedSignificantActivity")
    | where Activity == "EntraConnectSync"
    | summarize make_list(ActorId)
);
// let _UserInfo =
//     IdentityInfo
//     | where TimeGenerated > ago(query_period) and isnotempty(AccountObjectId) //and UserType == "Member"
//     | summarize arg_max(TimeGenerated, AccountUPN, MailAddress, AdditionalMailAddresses) by AccountObjectId
//     | project AccountObjectId, UserPrincipalName = AccountUPN, MailAddress, AdditionalMailAddresses
// ;
let _AddDeleteDeviceEvents =
    AuditLogs
    | where TimeGenerated between (ago(query_frequency + query_wait) .. ago(query_wait))
    | where LoggedByService == "Core Directory" and Category == "Device" and AADOperationType in ("Add", "Delete")
    | where not(ResultDescription == "Microsoft.Online.Workflows.ObjectAlreadyExistsException")
    | where not(tostring(InitiatedBy["user"]["id"]) in (_EntraConnectSyncAccounts))
    | where not(tostring(InitiatedBy["app"]["servicePrincipalId"]) in (_ExpectedDeviceManagementIds))
    | extend
        ObjectId = tostring(TargetResources[0]["id"]),
        DeviceName = tostring(TargetResources[0]["displayName"]),
        InitiatedByUser = tostring(InitiatedBy["user"]["userPrincipalName"]),
        InitiatedByApp = tostring(InitiatedBy["app"]["displayName"])
    | project TimeGenerated, Type, InitiatedByUser, InitiatedByApp, OperationName, Result, ResultDescription, DeviceName, ObjectId, AdditionalDetails, InitiatedBy, TargetResources, CorrelationId
    | mv-apply ModifiedProperty = TargetResources[0]["modifiedProperties"] on (
        summarize BagToUnpack = make_bag(bag_pack(replace_string(tostring(ModifiedProperty["displayName"]), "TargetId.", ""), translate(@'["\]', "", tostring(ModifiedProperty["newValue"]))))
    )
    | evaluate bag_unpack(BagToUnpack, columnsConflict = "replace_source")
    // | join kind=leftanti _ExpectedTrustTypes on DeviceTrustType, DeviceOSType
    | extend
        AutopilotZTDID = extract(@"ZTDID\:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        AccountObjectId = extract(@"USER\-GID\:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        ["USER-HWID"] = extract(@"USER\-HWID\:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        IntuneOrderId = extract(@"OrderId:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        AzureResourceId = extract(@"AzureResourceId:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", ""))
    // | lookup kind=leftouter _UserInfo on AccountObjectId
;
let _AddObjectIds = toscalar(_AddDeleteDeviceEvents
    | where OperationName == "Add device"
    | summarize make_set(ObjectId)
);
let _UpdateDeviceEvents =
    AuditLogs
    | where TimeGenerated > ago(query_frequency + query_wait)
    //| where OperationName in ("Update device", "Add member to group", "Remove member from group", "Device no longer compliant", "Device no longer managed")
    | where not(LoggedByService == "Core Directory" and Category == "Device" and AADOperationType in ("Add", "Delete", "Assign", "Unassign"))
    | where not(ResultDescription == "Microsoft.Online.DirectoryServices.DirectoryValueExistsException")
    | where isnotempty(_AddObjectIds) and tostring(TargetResources[0]["id"]) in (_AddObjectIds)
    | where not(tostring(InitiatedBy["user"]["id"]) in (_EntraConnectSyncAccounts))
    | extend
        ObjectId = tostring(TargetResources[0]["id"]),
        DeviceName = tostring(TargetResources[0]["displayName"]),
        InitiatedByUser = tostring(InitiatedBy["user"]["userPrincipalName"]),
        InitiatedByApp = tostring(InitiatedBy["app"]["displayName"])
    | project TimeGenerated, Type, InitiatedByUser, InitiatedByApp, OperationName, Result, ResultDescription, DeviceName, ObjectId, AdditionalDetails, InitiatedBy, TargetResources, CorrelationId
    | mv-apply ModifiedProperty = TargetResources[0]["modifiedProperties"] on (
        summarize BagToUnpack = make_bag(
            bag_pack(replace_string(tostring(ModifiedProperty["displayName"]), "TargetId.", ""),
                iff(OperationName has "Remove" and tostring(ModifiedProperty["displayName"]) startswith "Group",
                    translate(@'["\]', "", tostring(ModifiedProperty["oldValue"])),
                    translate(@'["\]', "", tostring(ModifiedProperty["newValue"])))))
    )
    | evaluate bag_unpack(BagToUnpack, columnsConflict = "replace_source")
    | extend
        AutopilotZTDID = extract(@"ZTDID\:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        AccountObjectId = extract(@"USER\-GID\:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        ["USER-HWID"] = extract(@"USER\-HWID\:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        IntuneOrderId = extract(@"OrderId:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", "")),
        AzureResourceId = extract(@"AzureResourceId:([^\:\,]+)", 1, column_ifexists("DevicePhysicalIds", ""))
    // | lookup kind=leftouter _UserInfo on AccountObjectId
;
let _RegisterUserToDeviceEvents =
    AuditLogs
    | where TimeGenerated > ago(query_frequency + query_wait)
    | where LoggedByService == "Core Directory" and Category == "Device" and AADOperationType in ("Assign", "Unassign")
    | where not(ResultDescription in ("Microsoft.Online.Workflows.ObjectAlreadyExistsException", "Microsoft.Online.DirectoryServices.DirectoryValueExistsException"))
    | where isnotempty(_AddObjectIds) and TargetResources[1]["id"] in (_AddObjectIds)
    | where not(tostring(InitiatedBy["user"]["id"]) in (_EntraConnectSyncAccounts))
    | extend
        ObjectId = tostring(TargetResources[1]["id"]),
        InitiatedByUser = tostring(InitiatedBy["user"]["userPrincipalName"]),
        InitiatedByApp = tostring(InitiatedBy["app"]["displayName"]),
        DeviceUser = pack(iff(OperationName has "registered owner", "Owner", "User"), tostring(TargetResources[0]["userPrincipalName"]))
    | project TimeGenerated, Type, InitiatedByUser, InitiatedByApp, OperationName, Result, ResultDescription, ObjectId, AdditionalDetails, InitiatedBy, TargetResources, CorrelationId, DeviceUser
    | extend ["Device.DisplayName"] = "", DeviceId = ""
    | mv-apply ModifiedProperty = TargetResources[0]["modifiedProperties"] on (
        summarize BagToUnpack = make_bag(
            bag_pack(tostring(ModifiedProperty["displayName"]),
                iff(OperationName has "Remove",
                    translate(@'["\]', "", tostring(ModifiedProperty["oldValue"])),
                    translate(@'["\]', "", tostring(ModifiedProperty["newValue"])))))
    )
    | evaluate bag_unpack(BagToUnpack, columnsConflict = "replace_source", ignoredProperties=dynamic(["Device.ObjectID"]))
    | project-rename DeviceName = ["Device.DisplayName"]
;
let _DeviceEvents = materialize(
    union _AddDeleteDeviceEvents, _UpdateDeviceEvents, _RegisterUserToDeviceEvents
    | extend
        DeviceTrustType = column_ifexists("DeviceTrustType", ""),
        CloudDisplayName = column_ifexists("CloudDisplayName", ""),
        DisplayName = column_ifexists("DisplayName", ""),
        ["Included Updated Properties"] = column_ifexists("Included Updated Properties", "")
    | project-rename IncludedUpdatedProperties = ["Included Updated Properties"]
    | as _Auxiliar
    | project-away DeviceId
    | lookup kind=inner (
        _Auxiliar
        | sort by TimeGenerated asc
        | summarize
            OperationNames = make_set(OperationName),
            DeviceId = take_any(column_ifexists("DeviceId", "")),
            take_anyif(AccountObjectId, OperationName == "Add device"),
            take_anyif(AzureResourceId, OperationName == "Add device"),
            take_anyif(AutopilotZTDID, OperationName == "Add device"),
            AddIntuneOrderId = take_anyif(IntuneOrderId, OperationName == "Add device"),
            UpdateIntuneOrderId = take_anyif(IntuneOrderId, OperationName == "Update device"),
            DeviceTrustTypes = make_set_if(DeviceTrustType, isnotempty(DeviceTrustType)),
            //LastAddTimeGenerated = maxif(TimeGenerated, OperationName == "Add device"),
            //LastDeleteTimeGenerated = maxif(TimeGenerated, OperationName == "Delete device"),
            LastDeviceName = tostring(make_list_if(coalesce(CloudDisplayName, DisplayName, DeviceName), IncludedUpdatedProperties has_any ("CloudDisplayName", "DisplayName"))[-1])
            by ObjectId
        | where OperationNames has "Add device"
        // Custom conditions
        //| where not(isnotempty(AccountObjectId) and isnotempty(AzureResourceId))
        //| where not(AddIntuneOrderId in (_ExpectedIntuneOrderIds) or UpdateIntuneOrderId in (_ExpectedIntuneOrderIds))
        //| where not(isnotempty(AutopilotZTDID))
        //| where not(array_length(DeviceTrustTypes) == 1 and tostring(DeviceTrustTypes[0]) == "Workplace") and isnotempty(LastDeleteTimeGenerated) and isnotempty(LastAddTimeGenerated) and LastDeleteTimeGenerated > LastAddTimeGenerated)
        | project ObjectId, DeviceId
    ) on ObjectId
);
let _DeviceIds = toscalar(_DeviceEvents | summarize make_set(DeviceId));
let _SignInEvents =
    union
        (SigninLogs
        | where TimeGenerated > ago(query_frequency + query_wait)
        | where DeviceDetail["deviceId"] in (_DeviceIds)
        ),
        (AADNonInteractiveUserSignInLogs
        | where TimeGenerated > ago(query_frequency + query_wait)
        | where DeviceDetail has_any (_DeviceIds)
        | extend DeviceDetail = todynamic(DeviceDetail)
        )
    | summarize
        arg_min(TimeGenerated,
            DeviceName = tostring(DeviceDetail["displayName"]),
            DeviceDetail,
            UserPrincipalName,
            CorrelationId,
            Type)
        by DeviceId = tostring(DeviceDetail["deviceId"]), UserId, IPAddress
;
union _DeviceEvents, _SignInEvents
| sort by DeviceId asc, TimeGenerated asc
| project-reorder
    TimeGenerated,
    Type,
    InitiatedByApp,
    OperationName,
    Result, 
    DeviceName,
    DeviceTrustType,
    UserPrincipalName,
    IPAddress,
    ObjectId,
    DeviceId,
    AutopilotZTDID,
    AccountObjectId,
    IntuneOrderId,
    AzureResourceId,
    AdditionalDetails,
    InitiatedBy,
    TargetResources,
    CorrelationId,
    IncludedUpdatedProperties
//};
//Function(query_frequency, query_period, query_wait)

Explanation

This query is designed to help detect unexpected devices in Entra ID (formerly Azure Active Directory). Here's a simplified breakdown of what it does:

  1. Parameters Setup: The query is set up to run over a specified period (default is 14 days) and waits for an hour before executing. These parameters can be adjusted when saving the query as a function.

  2. Expected Values: It checks against expected values for device trust types, device management IDs, Intune order IDs, and Entra Connect sync accounts. These expected values are retrieved from a watchlist called "Activity-ExpectedSignificantActivity."

  3. Device Events:

    • Add/Delete Events: It looks at audit logs for devices added or deleted in Entra ID, filtering out expected operations and those initiated by known accounts or apps.
    • Update Events: It examines updates to devices, excluding certain operations and ensuring the updates are related to recently added devices.
    • User Registration Events: It checks for user assignments to devices, focusing on unexpected registrations.
  4. Device Event Consolidation: It consolidates all device-related events, extracting relevant details like device IDs, names, trust types, and other identifiers.

  5. Sign-In Events: It checks sign-in logs for any activity involving the identified devices, capturing details like user principal names and IP addresses.

  6. Result Compilation: Finally, it combines all the device and sign-in events, sorting and projecting relevant fields for analysis.

This query is useful for security teams to identify and investigate devices that may not conform to expected configurations or behaviors within their Entra ID environment.

Details

Jose Sebastián Canós profile picture

Jose Sebastián Canós

Released: June 23, 2025

Tables

_GetWatchlistAuditLogsSigninLogsAADNonInteractiveUserSignInLogs

Keywords

EntraIDDevicesIntuneUserAuditLogsAzureADDeviceManagementSigninLogsAADNonInteractiveUserSignInLogs

Operators

lettoscalarwheremv-expandsplittostringprojectsummarizemake_listmake_bagbag_packreplace_stringtranslateiffcolumn_ifexistsextractisnotemptynotinextendproject-awaylookupkindinnersort bytake_anytake_anyifmake_setmake_set_ifcoalescehas_anyarray_lengtharg_minunionhastodynamicproject-reorder

Actions