Let me start with a little story behind the reason for this post 🙂
This morning I was happily working away when my VPN connection dropped. I noticed the icon of the VPN software disappear from the notification area and I immediately recognised the behaviour – the VPN client was getting an upgrade.
As it happens, we use Patch My PC with Intune to update this software, and in order to accurately target devices we synchronize collections in ConfigMgr with Azure AD groups that Intune / PMPC can then use for application assignments (yes I know filters are preferred but they don’t give us the granularity we need at the moment).
When I contacted my colleague James about the upgrade, he insisted that he had not targeted my PC and in fact had cleaned out the associated collection in ConfigMgr a few days ago and just added a few test devices. I checked ConfigMgr – he was correct, just 3 test devices. I checked the AAD group that collection was synced with, and behold more than 300 devices were present including my own. Now we have a rogue deployment to deal with!
So how does that happen? The devices were not manually added to the AAD group, it’s only getting populated by the ConfigMgr sync, so I can only conclude that placing our trust in the reliability of the collection synchronization process was perhaps a tad optimistic. Our friend Google confirmed I was not alone.
After reviewing the membership of our other synced collections and their AAD groups, I was surprised to find a reasonable amount of disparity between the two. Of course, some disparity is not unexpected, for example you may have added devices to the collection that are not co-managed and Intune enrolled, or are not Azure AD joined (or hybrid), or are not Windows 10/11 operating systems. There are also devices that have gone stale and have been cleaned up in ConfigMgr but the associated Intune/AAD device records have not yet been cleaned, so they remain in the AAD group.
When creating a collection synced with an AAD group, the first time you create the sync it runs a full synchronization, which does both an add and remove (if necessary), overwriting the membership of the AAD group. After then, just the incremental synchronization runs, which will also add and remove, but it will not overwrite the membership of the group. Microsoft’s recommendation of course, is that ConfigMgr be the authority for those AAD groups – rightly so, but there’s no mechanism to prevent the collections and groups from being independently updated.
Additionally, relying on the incremental sync alone is apparently not such a good idea and it doesn’t guarantee that the collections and groups will remain in sync.
All that to say this: schedule a regular full synchronization of your synced collections. Because a full sync overwrites the AAD group membership, this helps to mitigate where the incremental process may fail at times or where the collections and groups may be independently updated, as well as removing stale devices.
So how do you schedule a regular full sync? You can’t!
No, I’m kidding. Actually – at the time of writing – you can’t just right-click the collection in the console and set a sync schedule as you can an evaluation schedule. You could initiate a one-time full sync using the Synchronize Membership option:
There is, however, a WMI method that can initiate a full sync, and you can simply create a scheduled task on the site server to run this regularly. The PowerShell code below will get all the collections that are synced with AAD groups and initiate a full sync for each of them (I take no credit for the code which is borrowed from here).
$SiteCode = "ABC"
$Instances = Get-CimInstance -Namespace "root\sms\site_$SiteCode" -ClassName SMS_CollectionAADGroupMapping
foreach ($Instance in $Instances)
{
Invoke-CimMethod -Namespace "root\sms\site_HET" -ClassName SMS_CollectionAADGroupMapping -MethodName ConfigSyncSettings -Arguments @{CollectionSiteID = $Instance.CollectionSiteID}
# or using the ConfigMgr module cmdlets
# Invoke-CMWmiMethod -ClassName SMS_CollectionAADGroupMapping -MethodName ConfigSyncSettings -Parameter @{CollectionSiteID = $Instance.CollectionSiteID}
}
The behaviour seems to be that it sets the “Flag” property of the mapped collection to “1”, then the next time the incremental sync process runs (every 5 minutes), it will actually do a full sync of the collection instead.
Bear in mind that if you have several synced collections with a large number of members, the full sync process can take some time. I run a daily full sync outside of busy working hours.
If you want to compare the membership counts of your synced collections with their AAD groups, you can use the following code. It will query the ConfigMgr database for all the collections that are synced with AAD groups, query each of those AAD groups using Microsoft Graph REST api and get the transitive membership count of each (ie including members of any nested groups which may (but shouldn’t!) exist), then output everything to Grid view, where you can see at a glance whether your collections and groups are in sync.
The code assumes you have the appropriate permissions to both the ConfigMgr database and Microsoft Graph with your user account, and requires the Microsoft.Graph.Intune PS module.
# MEMCM database params
$dataSource = 'MyConfigMgrSQLServer'
$database = 'CM_XXX'
# SQL query
$Query = "
Select
aad.CollectionSiteID,
col.CollectionID,
col.CollectionName,
col.ObjectPath,
aad.AADGroupID,
aad.AADGroupName,
col.MemberCount as 'CollectionMemberCount'
from dbo.vSMS_SCCMCollAADGroupMapping aad
join dbo.v_Collections col on aad.CollectionSiteID = col.SiteID
order by col.CollectionName
"
# Query the database
$connectionString = "Server=$dataSource;Database=$database;Integrated Security=SSPI;"
$connection = New-Object -TypeName System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$command = $connection.CreateCommand()
$command.CommandText = $Query
$reader = $command.ExecuteReader()
$table = New-Object -TypeName 'System.Data.DataTable'
$table.Load($reader)
$connection.Close()
# Add a couple of columns
[void]$table.Columns.Add("AADGroupMemberCount",[System.Int32])
[void]$table.Columns.Add("MembershipsInSync",[System.String])
# Authenticate with MS Graph
$ProgressPreference = 'SilentlyContinue'
$script:GraphToken = Connect-MSGraph -PassThru
# Function to call the Graph REST API with error handling
Function script:Invoke-LocalGraphRequest {
Param ($URL,$Headers,$Method)
try {
$WebRequest = Invoke-WebRequest -Uri $URL -Method $Method -Headers $Headers -UseBasicParsing
}
catch {
$WebRequest = $_.Exception.Response
}
Return $WebRequest
}
# Function to get the transitive member count of an AAD group (ie including nested group membership)
Function Get-AADGroupTransitiveMemberCount {
Param($GroupId)
$URL = "https://graph.microsoft.com/v1.0/groups/$GroupId/transitiveMembers/`$count"
$headers = @{'Authorization'="Bearer " + $GraphToken;'ConsistencyLevel'="eventual"}
$GraphRequest = Invoke-LocalGraphRequest -URL $URL -Headers $headers -Method GET
return $GraphRequest
}
# Process each collection/group
foreach ($row in $table.Rows)
{
$Id = $Row.AADGroupID
$Result = Get-AADGroupTransitiveMemberCount -GroupId $Id
If ($Result.StatusCode -eq 200)
{
if ($Result.Content.Length -gt 0)
{
[int]$Count = $Result.Content
}
else
{
[int]$Count = 0
}
$row.AADGroupMemberCount = $Count
If ($row.CollectionMemberCount -eq $row.AADGroupMemberCount)
{
$row.MembershipsInSync = "Yes"
}
else
{
$row.MembershipsInSync = "No"
}
}
else
{
$row.MembershipsInSync = "Unknown. Error code $($result.statuscode) received on graph request."
}
}
$table.rows | Out-GridView