Intro
Every couple of weeks I see a Reddit post or question asking about device group memberships or filters for certain properties that Azure AD doesn’t natively contain. One common request is making a dynamic group for all desktops or all laptops. There may be other specific properties about your devices that Azure AD doesn’t allow dynamic membership queries for. This is where we can use extension attributes to add custom attribute values to our devices. Extension attributes can be used in Dynamic Group queries and when filtering for devices in conditional access policies, making them very useful and versatile for certain use cases.
Adding an extension attribute to a single device is fairly simple using graph explorer Configuring extension attributes for devices in Azure AD – Blog (michev.info). However, what if you have hundreds of thousands of devices where you need to add extension Attributes based on specific properties of the devices? We can accomplish this with some PowerShell scripting and Remediations (formerly known as proactive remediations). I had a couple of challenges when coming up with this solution:
- Automating the Graph authentication on each device. Luckily, I found a post by Liam Clearly that solves this issue pretty quickly. The link and more details are below when discussing the scripts.
- Securing the scripts, since they use an app reg secret key. We can use conditional access for service principals, but we only have the options of sign-in risk and location right now. I’d love for them to add compliant devices in the future, which would make this solution much more secure.
In this example, we will use the solution to identify if a machine has a battery, and if so, add the value of Laptop to extensionAttribute1. If it does not have a battery, we will add the value of Desktop. This is a simple example, but it can be modified to use just about any properties you can retrieve with PowerShell that are not natively part of Azure AD/Intune. There are three main components:
- The App Registration that automates the authentication to the Graph API
- The detection script
- The remediation script
The App Registration
Create your app registration in Azure. This is straightforward. Provide a name, and then grant the below permissions. I’ll use one App Reg for both the detection and remediation scripts. Add the below permissions:
- Device.Read.All
- Device.ReadWrite.All
- Directory.Read.All
Next, create your client secret with your desired expiration date. Make sure to record your secret somewhere safe.
That wraps up the App registration. If you’re not familiar with authenticating with an App Reg, in addition to the client secret value, you’ll also need the tenant ID, and the client ID. Both of those are easily retrieved from the App Registration’s overview:
The Detection Script:
Moving on to our scripts, the first issue I encountered is there is no native way with the PowerShell graph modules to connect using an app registration’s secret key. After some searching online, I came across this blog post which resolves the issue – Connect to Microsoft Graph PowerShell using an App Registration – Liam Cleary [MVP and MCT] (helloitsliam.com). Now that we had a way to connect using our app registration, we can write our detection script. Note that this was all performed in a non-production lab environment. App Registration secrets in plain text is a security risk. Make sure you’re aware of security risks if performing in a production environment or consider an alternative solution.
The script looks for a WMI property, in this case, if a battery is present, and then determines if the extensionattribute is already set on the computer object in Azure AD. If not, it will fail detection and the remediation script will run. The detection script is shown below and also available on Github here. You’ll need to make some changes depending on what you’re trying to identify, but the important part is to make sure your $attributevalue is set properly under the if statements, depending on the output from the $battery variable. This is what will be compared against the device attributes in Graph for the device.
Write-Host "Checking for required module"
$module = get-module -listavailable -name 'Microsoft.Graph.Identity.DirectoryManagement' -ErrorAction SilentlyContinue
if ($module -eq $null) {
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Force
}
$appid = 'your app ID'
$tenantid = 'your tenant ID'
$secret = 'your secret'
$body = @{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
Client_Id = $appid
Client_Secret = $secret
}
$connection = Invoke-RestMethod `
-Uri https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token `
-Method POST `
-Body $body
$token = $connection.access_token
Connect-MgGraph -AccessToken $token
$DeviceId = Get-MgDevice | where DisplayName -eq $env:computername | Select-Object -ExpandProperty Id
$properties = get-mgdevice -DeviceId $deviceID | select-object -expandproperty AdditionalProperties
$extensionattributes = $properties.extensionAttributes | FT -HideTableHeaders | Out-String
$battery = Get-WmiObject Win32_Battery
if ($battery -eq $null) {
$attributevalue = "extensionAttribute1 Desktop"
}
if ($battery -ne $null) {
$attributevalue = "extensionAttribute1 Laptop"
}
if ($extensionattributes.Trim() -match $attributevalue) {
write-output "Correct Attribute Assigned"
exit 0
}
else {
write-output "Attribute missing or incorrect"
Exit 1
}
The Remediation Script
Next, we have the remediation script. Similar to the detection script, we use the app registration to connect to Graph, but this time we are writing a value for extensionAttribute1 as “Laptop” or “Desktop” based on the presence of a battery. We use a similar if statement that checks the output from the $battery variable, and then sets extensionAttribute1 accordingly.
Write-Host "Checking for required module"
$module = get-module -listavailable -name 'Microsoft.Graph.Identity.DirectoryManagement' -ErrorAction SilentlyContinue
if ($module -eq $null) {
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Force
}
$appid = 'your app ID'
$tenantid = 'your tenant ID'
$secret = 'your secret'
$body = @{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
Client_Id = $appid
Client_Secret = $secret
}
$connection = Invoke-RestMethod `
-Uri https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token `
-Method POST `
-Body $body
$token = $connection.access_token
Connect-MgGraph -AccessToken $token
$laptop = '{ "extensionAttributes": { "extensionAttribute1": "Laptop" } }'
$desktop = '{ "extensionAttributes": { "extensionAttribute1": "Desktop" } }'
$battery = Get-WmiObject Win32_Battery
$DeviceId = Get-MgDevice | where DisplayName -eq $env:computername | Select-Object -ExpandProperty Id
if ($battery -eq $null) {
Update-MgDevice -DeviceId $DeviceId -BodyParameter $desktop
}
if ($battery -ne $null) {
Update-MgDevice -DeviceId $DeviceId -BodyParameter $laptop
}
Disconnect-MgGraph
The Intune Remediation
Lastly, lets create our remediation. Provide a name and description.
Add the detection and remediation scripts:
Assign to your groups and create a schedule:
After it runs against some devices, you’ll see the device status output showing the remediation was correctly ran, setting the extension attribute.
We can look at Azure AD to see an individual device extension attributes, and we can see here that the remediation properly set this machine as a desktop since it does not contain a battery:
This feels like a huge security risk adding a secret in clear text available on the device, that has Directory.ReadWrite.All.
Honestly, don’t do this.
Thanks Nickolaj. Some others pointed this out also. That permission is not required for this, and I’ve fixed that portion of the post. I agree that app reg secrets are not always the best way. If they are needed, definitely need to make sure they’re handled carefully and use least privilege.
I am sorry, but having a secret in clear text with Directory.ReadWrite.All is not something I would ever do. It’s like giving Global Admin to everyone. I see what you are trying to do, but this is not the way!
Thanks, Jan, for pointing that out. I definitely agree. I wrote this over a few weeks testing on and off, and mistakenly didn’t update that portion. That permission (directory.readwrite.all) is not required, and I fixed that portion of the post. Anything with an app reg secret should be handled very carefully and use least privilege.