The Tale of the Companion App

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!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.