Once upon a time, I stumbled upon a challenge.. we needed to ensure that all devices with a specific app installed also had a required companion app. The scenario is – as you might already guess – that the specific app in question needs to connect through a VPN to get its job done. Specifically, we had a couple of apps that required the Microsoft Defender (Tunnel) configuration to function properly.
I started on this a while ago, but couldn’t find a good way to approach this on the managed devices. Turns out, I had looked at it the wrong way. When I found a script that Andrew Taylor (Quick and easy application Inventory with Intune – andrewstaylor.com) had written to browse and drill down in the Detected Apps Graph lists, and list the devices from there, I got inspiration for a new direction.
In this post, I’ll share how I created a script to accomplish this using the necessary Graph functions in PowerShell.
In a future post I’ll show you how you can automate this process using Azure Function Apps.
The Challenge
The objectives are pretty clear:
- Identify devices with a particular app installed in Microsoft Intune
- Retrieve the users associated with those devices
- Add those users to an Entra ID group
- Deploy the required Microsoft Defender (Tunnel) companion app to that group
Traditional methods like static or dynamic groups didn’t cut it. Static groups that were there for other purposes and couldn’t be touched, and managing them manually isn’t really scalable. Dynamic groups couldn’t filter on installed applications since they don’t have visibility into Intune’s app inventory.
The Solution
To solve this, I started on a PowerShell script (5.2 compatible) and did some test runs through the Graph module Microsoft.Graph.DeviceManagement and Get-MgDeviceManagementDetectedApp.
PS C:\Users\HaakonWibe> Connect-MgGraph
PS C:\Users\HaakonWibe> Get-MgDeviceManagementDetectedApp -Top 5
Id DeviceCount DisplayName Platform Publisher
-- ----------- ----------- -------- ---------
7db4d4605300c4195af22a9b8538784c0ec0c06ae6bb8b9914b47b41cc338261 1 8bitSolutionsLLC.bitwardendesktop windows CN=14D52771-DE3C-...
6a02e5055e1b922ded9a8f6e8660a9ea60418cf1a3fe77b28f516b5883e24282 1 AppUp.IntelGraphicsExperience windows CN=EB51A5DA-0E72-...
0000bee1690c0ad4d85047827c1c545565330000ffff 1 CPUID HWMonitor 1.54 windows CPUID, Inc.
06933f13d10e6db14694306fc45da27362c01f4a2ae56e1dec779fd25de77a4f 1 Clipchamp.Clipchamp windows CN=33F0F141-36F3-...
e5babdb7edcca1171cfc5e8d228bda3fac7af20fda942e78638646f85fbc7e37 1 Clipchamp.Clipchamp windows CN=33F0F141-36F3-...
PS C:\Users\HaakonWibe> $appName = "Discord"
PS C:\Users\HaakonWibe> $detectedApps = Get-MgDeviceManagementDetectedApp -All
PS C:\Users\HaakonWibe> $targetApp= $detectedApps | Where-Object { $_.DisplayName -eq $appName }
PS C:\Users\HaakonWibe> $targetApp
Id DeviceCount DisplayName Platform Publisher SizeInByte Version
-- ----------- ----------- -------- --------- ---------- -------
000088104ac88abcc43e181d8ef4f831dc1800000904 1 Discord windows Discord Inc. 0 1.0.9003
So, we can see that after connecting to Graph, we can use the Get-MgDeviceManagementDetectedApp
cmdlet to list all detected apps in our environment. By default it won’t show you all, unless you specify it with the -All
parameter, but it will show you the top 100.
Since we need to look at the entire inventory, we’ll fetch it all and put it in an array. This way, we can filter it and put the results into a new variable.
This gives us the target app we want in our inventory. But this doesn’t help us a lot just yet – we need to know which devices the app is installed on? For that, there is a cmdlet on a level deeper than our previous command called Get-MgDeviceManagementDetectedAppManagedDevice
. It requires an AppId, so where can we get this from? Expanding the properties on our $matchingApps variable will provide that.
PS C:\Users\HaakonWibe> $targetApp | fl
DeviceCount : 1
DisplayName : Discord
Id : 000088104ac88abcc43e181d8ef4f831dc1800000904
ManagedDevices :
Platform : windows
Publisher : Discord Inc.
SizeInByte : 0
Version : 1.0.9003
AdditionalProperties : {}
Here we can see the “Id : 000088104ac88abcc43e181d8ef4f831dc1800000904”. Let’s punch that into our new cmdlet.
PS C:\Users\HaakonWibe> Get-MgDeviceManagementDetectedAppManagedDevice -DetectedAppId 000088104ac88abcc43e181d8ef4f831dc1800000904
Id ActivationLockBypassCode AndroidSecurityPatchLevel AzureAdDeviceId AzureAdRegister
ed
-- ------------------------ ------------------------- --------------- ---------------
c06b34b2-eb47-430d-90d9-a683dcdea41a
Looks like we found a device candidate with the app we’re searching for installed.
Selecting some more relevant properties shows us that we’re looking at a Windows 10 device.
PS C:\Users\HaakonWibe> Get-MgDeviceManagementDetectedAppManagedDevice -DetectedAppId 000088104ac88abcc43e181d8ef4f831dc1800000904 | Select-Object Id, DeviceName, OperatingSystem, OSVersion
Id DeviceName OperatingSystem OSVersion
-- ---------- --------------- ---------
c06b34b2-eb47-430d-90d9-a683dcdea41a HAWKWV-MP1B9BWT Windows 10.0.19045.5011
Right, so now we have gotten further, and we have an actual device with our app installed on.
Strings and arrays
So I’d like to just pause here for a moment and have a look on an error I got while trying to look up a more common app. When I asked for the app ID of the app with $appId = $targetApp.ID, I got the following result
PS C:\Users\HaakonWibe> $appid
54106b1b302d39d2c3105d693251fdb1191d5c851ed349396d22a009a19b3121
9ae218a045d9bf54c620e4c81ec01d0dc9d5d49ad418e8ecff5441501f5fcca2
f8a9fdd47c758135609cb5ddc98fb91256ea6a9ae76db63751b04b96fb786a4a
That’s three different app IDs. When I then try to pass that to the command Get-MgDeviceManagementDetectedAppManagedDevice as values to the parameter -DetectedAppID, it throws an error saying it can’t convert the value to type System.String. That’s because it expects a – well String – and we’re trying to feed it an array.
PS C:\Users\HaakonWibe> Get-MgDeviceManagementDetectedAppManagedDevice -DetectedAppId $appId -All
Get-MgDeviceManagementDetectedAppManagedDevice : Cannot process argument transformation on parameter 'DetectedAppId'. Cannot convert value to type System.String.
We could start filtering to narrow down to a single app. For example, if I know the version or the publisher that could help in selecting just one app. We could also validate by exiting if there’s more than one app, or selecting the first or top one. But that isn’t very practical, so let’s rewrite what we have so far to allow for multiple apps.
$appName = "YourAppName"
$detectedApps = Get-MgDeviceManagementDetectedApp -All
$matchingApps = $detectedApps | Where-Object { $_.DisplayName -eq $appName }
if ($null -eq $matchingApps -or $matchingApps.Count -eq 0) {
Write-Host "App '$appName' not found."
exit
}
To avoid returning null values, we’ll include a simple “isnull” check.
Let’s see what we’ve found and remind ourselves how many of them there are:
PS C:\Users\HaakonWibe> Write-Host "Found $($matchingApps.Count) apps matching '$appName'."
Found 1 apps matching 'Discord'.
Drilling down to devices and users
Now on to the actual devices. For that we’ll initialize an array and starting pulling devices and add them to the array.
$allAssociatedManagedDevices = @()
foreach ($app in $matchingApps) {
$appId = $app.Id
Write-Host "Processing App ID: $appId, Version: $($app.Version), Publisher: $($app.Publisher)"
$associatedManagedDevices = Get-MgDeviceManagementDetectedAppManagedDevice -DetectedAppId $appId -All
if ($associatedManagedDevices) {
$allAssociatedManagedDevices += $associatedManagedDevices
}
}
Write-Host "Total managed devices found: $($allAssociatedManagedDevices.Count)"
PS C:\Users\HaakonWibe> Write-Host "Total managed devices found: $($allAssociatedManagedDevices.Count)"
Total managed devices found: 1
This is good! We have the device(s), but it’s the users we really want to ultimately add to the user group. Let’s follow the same approach for the users as for the devices:
$userIds = @()
foreach ($device in $allAssociatedManagedDevices) {
$managedDevice = Get-MgDeviceManagementManagedDevice -ManagedDeviceId $device.Id
if ($managedDevice.UserId -ne $null -and $managedDevice.UserId -ne "") {
$userIds += $managedDevice.UserId
}
}
To avoid getting errors showing up in the logs when adding the users to the group later, it’s best to clean up the list of users just to be sure:
PS C:\Users\HaakonWibe> $userIds = $userIds | Select-Object -Unique
PS C:\Users\HaakonWibe>
PS C:\Users\HaakonWibe> Write-Host "Total unique users to add: $($userIds.Count)"
Total unique users to add: 1
What about our group?
So now that we have the userIds, why don’t we go ahead and add them to our group. The Microsoft Graph cmdlets natively work best with group IDs, so let’s start by finding the ID of the group I created before to assign the Defender app.
PS C:\Users\HaakonWibe> Get-MgGroup -Filter "displayname eq 'APP-INTUNE-VPNAPP-COMPANION'"
DisplayName Id MailNickname Description
----------- -- ------------ -----------
APP-INTUNE-VPNAPP-COMPANION 1b38a50e-506b-443f-9cf2-07fadf13ccfc fc73ad07-9 Group used to automatically add users ...
Then with our newly discovered ID, we can use that in a new foreach loop which adds our users to the group:
PS C:\Users\HaakonWibe> $groupID = Get-MgGroup -Filter "displayname eq 'APP-INTUNE-VPNAPP-COMPANION'" | Select-Object Id
foreach ($userId in $userIds) {
try {
# Check if the user is already a member of the group
$isMember = Get-MgGroupMember -GroupId $groupId -Filter "id eq '$userId'" -ErrorAction SilentlyContinue
if ($isMember -eq $null) {
New-MgGroupMember -GroupId $groupId -DirectoryObjectId $userId
Write-Host "Added user with ID $userId to group $groupId"
} else {
Write-Host "User with ID $userId is already a member of the group."
}
} catch {
Write-Host "Failed to add user with ID $userId to group $groupId. Error: $_"
}
}
The result:
PS C:\Users\HaakonWibe> Get-MgGroupMemberAsUser -GroupId 1b38a50e-506b-443f-9cf2-07fadf13ccfc
DisplayName Id Mail UserPrincipalName
----------- -- ---- -----------------
Haakon Wibe 14604253-5c8c-497a-9f62-d81ec5881c02 haakon.wibe@demotenant.com haakon.wibe@demotenant.com
Conclusion
So that’s it – the script has succesfully added the users who had the specific apps installed to a new group which makes sure the Defender app is pushed. The next step is to make sure this can run automated and hands-off, as well as make some improvements to this script with improved logging, error handling, and so on.
Stay tuned for more insights on automation and device management. If you have questions or comments, feel free to reach out!