Detection rules › Kusto
Suspicious Entra ID Joined Device Update
This query looks for suspicious updates to an Microsoft Entra ID joined device where the device name is changed and the device falls out of compliance. This could occur when a threat actor updates the details of an Autopilot provisioned device using a stolen device ticket, in order to access certificates and keys. Ref: https://dirkjanm.io/assets/raw/Insomnihack Breaking and fixing Azure AD device identity security.pdf
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | T1528 Steal Application Access Token |
Event coverage
| Provider | Event |
|---|---|
| Entra-AuditLogs | Update device |
Rule body kusto
id: 3a3c6835-0086-40ca-b033-a93bf26d878f
name: Suspicious Entra ID Joined Device Update
description: |
'This query looks for suspicious updates to an Microsoft Entra ID joined device where the device name is changed and the device falls out of compliance.
This could occur when a threat actor updates the details of an Autopilot provisioned device using a stolen device ticket, in order to access certificates and keys.
Ref: https://dirkjanm.io/assets/raw/Insomnihack%20Breaking%20and%20fixing%20Azure%20AD%20device%20identity%20security.pdf'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1528
query: |
AuditLogs
| where OperationName =~ "Update device"
| mv-apply TargetResource=TargetResources on (
where TargetResource.type =~ "Device"
| extend ModifiedProperties = TargetResource.modifiedProperties
| extend DeviceId = TargetResource.id)
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "CloudDisplayName"
| extend OldName = Prop.oldValue
| extend NewName = Prop.newValue)
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "IsCompliant"
| extend OldComplianceState = Prop.oldValue
| extend NewComplianceState = Prop.newValue)
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "TargetId.DeviceTrustType"
| extend OldTrustType = Prop.oldValue
| extend NewTrustType = Prop.newValue)
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "Included Updated Properties"
| extend UpdatedProperties = Prop.newValue)
| extend OldDeviceName = tostring(parse_json(tostring(OldName))[0])
| extend NewDeviceName = tostring(parse_json(tostring(NewName))[0])
| extend OldComplianceState = tostring(parse_json(tostring(OldComplianceState))[0])
| extend NewComplianceState = tostring(parse_json(tostring(NewComplianceState))[0])
| 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 UpdatedPropertiesCount = array_length(split(UpdatedProperties, ','))
| where OldDeviceName != NewDeviceName
| where OldComplianceState =~ 'true' and NewComplianceState =~ 'false'
// Most common is transferring from AAD Registered to AAD Joined - we just want AAD Joined devices
| where NewTrustType == '"AzureAd"' and OldTrustType != '"Workplace"'
// We can modify this value to tune FPs - more properties changed about the device beyond its name the more suspicious it could be
| where UpdatedPropertiesCount > 1
| project-reorder TimeGenerated, DeviceId, NewDeviceName, OldDeviceName, NewComplianceState, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, AADOperationType, OldTrustType, NewTrustType, UpdatedProperties, UpdatedPropertiesCount
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: NewDeviceName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: OldDeviceName
- entityType: Host
fieldMappings:
- identifier: AzureID
columnName: DeviceId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatedByName
- identifier: UPNSuffix
columnName: InitiatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
alertDetailsOverride:
alertDisplayNameFormat: Suspicious Entra ID Joined Device Update {{OldDeviceName}} renamed to {{NewDeviceName}} and {{UpdatedPropertiesCount}} properties changed
alertDescriptionFormat: |
This query looks for suspicious updates to an Microsoft Entra ID joined device where the device name is changed and the device falls out of compliance.
In this case {{OldDeviceName}} was renamed to {{NewDeviceName}} and {{UpdatedPropertiesCount}} properties were changed.
This could occur when a threat actor updates the details of an Autopilot provisioned device using a stolen device ticket, in order to access certificates and keys.
Ref: https://dirkjanm.io/assets/raw/Insomnihack%20Breaking%20and%20fixing%20Azure%20AD%20device%20identity%20security.pdf
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName =~ "Update device"
Stage 3: kusto:mv-apply
| mv-apply TargetResource=TargetResources on (
where TargetResource.type =~ "Device"
| extend ModifiedProperties = TargetResource.modifiedProperties
| extend DeviceId = TargetResource.id)
Stage 4: kusto:mv-apply
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "CloudDisplayName"
| extend OldName = Prop.oldValue
| extend NewName = Prop.newValue)
Stage 5: kusto:mv-apply
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "IsCompliant"
| extend OldComplianceState = Prop.oldValue
| extend NewComplianceState = Prop.newValue)
Stage 6: kusto:mv-apply
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "TargetId.DeviceTrustType"
| extend OldTrustType = Prop.oldValue
| extend NewTrustType = Prop.newValue)
Stage 7: kusto:mv-apply
| mv-apply Prop=ModifiedProperties on (
where Prop.displayName =~ "Included Updated Properties"
| extend UpdatedProperties = Prop.newValue)
Stage 8: extend (10 consecutive steps)
| extend OldDeviceName = tostring(parse_json(tostring(OldName))[0])
| extend NewDeviceName = tostring(parse_json(tostring(NewName))[0])
| extend OldComplianceState = tostring(parse_json(tostring(OldComplianceState))[0])
| extend NewComplianceState = tostring(parse_json(tostring(NewComplianceState))[0])
| 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 UpdatedPropertiesCount = array_length(split(UpdatedProperties, ','))
Stage 9: where
| where OldDeviceName != NewDeviceName
Stage 10: where
| where OldComplianceState =~ 'true' and NewComplianceState =~ 'false'
Stage 11: where
| where NewTrustType == '"AzureAd"' and OldTrustType != '"Workplace"'
Stage 12: where
| where UpdatedPropertiesCount > 1
Stage 13: project-reorder
| project-reorder TimeGenerated, DeviceId, NewDeviceName, OldDeviceName, NewComplianceState, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, AADOperationType, OldTrustType, NewTrustType, UpdatedProperties, UpdatedPropertiesCount
Stage 14: extend
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
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 |
|---|---|---|
NewComplianceState | eq |
|
NewTrustType | eq |
|
OldComplianceState | eq |
|
OldDeviceName | ne |
|
OldTrustType | ne |
|
OperationName | eq |
|
UpdatedPropertiesCount | gt |
|
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 |
|---|---|
OldDeviceName | extend |
NewDeviceName | extend |
OldComplianceState | extend |
NewComplianceState | extend |
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
UpdatedPropertiesCount | extend |
InitiatedByName | extend |
InitiatedByUPNSuffix | extend |