Skip to content

Instantly share code, notes, and snippets.

@NielsS79
Last active July 12, 2022 11:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NielsS79/e664c3d71d4727e6407a54815bd2aa8f to your computer and use it in GitHub Desktop.
Save NielsS79/e664c3d71d4727e6407a54815bd2aa8f to your computer and use it in GitHub Desktop.
Performs a find/replace operation on the group tag of Autopilot devices and (optionally) corrects dynamic group membership queries referencing it.
<#
.SYNOPSIS
Search and replace Windows Autopilot group tags over devices and dynamic group membership queries.
.DESCRIPTION
If you're like me, you'll frequently use Autopilot group tags to group devices in AAD groups. And, also if you're like me, you'll change your mind about the naming every so often.
This script will search and replace group tags assigned to devices _and_ correct your group membership queries in one go.
Set-AutopilotGroupTag and Get-AutopilotDevices are seperate helper functions, but you can use them as you wish. They are pretty straight-forward.
.PARAMETER $SearchFor
The text to look for.
.PARAMETER $ReplaceWith
The text to replace $SearchFor with.
.PARAMETER $Operator
The comparison operator to use when filtering devices by Autopilot group tag.
Possible values are 'equals','startswith','endswith' or 'contains'.
Defaults to 'equals'.
.PARAMETER $UpdateDynamicGroupQueries
If present, all dynamic group membership queries in Azure AD are processed.
This will _only_ look for $SearchFor when preceded by '[OrderId]:' to make sure we only replace Autopilot group tag references.
The comparison operator specified by $Operator is _not_ respected here. It is comparable to 'startswith' or 'equals'.
.PARAMETER $NoDisconnect
If present, this will prevent Azure AD and Graph sessions from disconnecting after execution.
.NOTES
Author: Niels Scheffers <niels.scheffers@etesian.nl>
Last modified: 2022-03-14
#>
Function Set-AutopilotGroupTag {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$true)]
[string]$AutopilotDeviceId,
[Parameter(Mandatory=$true)]
[string]$GroupTag
)
$uriPrefix = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities';
$uri = "$($uriPrefix)/$($AutopilotDeviceId)/UpdateDeviceProperties";
$json = @{groupTag = $GroupTag} | ConvertTo-Json;
if ($PSCmdlet.ShouldProcess($AutopilotDeviceId, 'Change Autopilot device''s group tag')) {
Invoke-MgGraphRequest -Uri $uri -Method POST -Body $json;
}
}
Function Get-AutopilotDevices {
Param(
[Parameter(Mandatory=$false)]
[string]$GroupTag = $null,
[Parameter(Mandatory=$false)]
[string]$Operator='equals'
)
# Sanitize input.
$Operator = $Operator.ToLower();
if ($Operator -notin @('equals','startswith','endswith','contains')) {
Throw "The '$($Operator)' is not supported. Please use 'equals','startswith','endswith' or 'contains'.";
}
$uriPrefix = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities';
if ($GroupTag) {
# Add filter for Group Tag.
# Unfortunately the 'eq' operator doesn't seem to work in this $filter, so we use contains().
$GroupTag = $GroupTag.ToLower();
$uri = "$($uriPrefix)?`$filter=contains(groupTag,'$([uri]::EscapeDataString($GroupTag))')";
}
$apDevicesFiltered = @();
$apDevices = (Invoke-MgGraphRequest -Uri $uri);
if ($apDevices -and $apDevices.Count -gt 0) {
foreach($apDevice in $apDevices.value) {
# If we are searching group tags with a contains() $filter (see above). We still need to evaluate these results by the operator specified.
if ($GroupTag) {
switch($Operator) {
'equals' {
if ((-not($GroupTag)) -or ($apDevice.groupTag.ToLower() -eq $GroupTag)) {
$apDevicesFiltered += $apDevice;
}
}
'startswith' {
if ((-not($GroupTag)) -or ($apDevice.groupTag.ToLower() -like "$($GroupTag)*")) {
$apDevicesFiltered += $apDevice;
}
}
'endswith' {
if ((-not($GroupTag)) -or ($apDevice.groupTag.ToLower() -like "*$($GroupTag)")) {
$apDevicesFiltered += $apDevice;
}
}
'contains' {
if ((-not($GroupTag)) -or ($apDevice.groupTag.ToLower() -like "*$($GroupTag)*")) {
$apDevicesFiltered += $apDevice;
}
}
}
}
}
}
return $apDevicesFiltered;
}
Function Replace-AutopilotGroupTag {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$true, HelpMessage='Enter the text to match.')]
[string]$SearchFor,
[Parameter(Mandatory=$true, HelpMessage='Enter the text to replace matches with.')]
[string]$ReplaceWith,
[Parameter(Mandatory=$false, HelpMessage='Enter the comparison operator you want to use. Defaults to ''equals''.')]
[string]$Operator='equals',
[Parameter(Mandatory=$false, HelpMessage='Use this switch to also process dynamic group membership queries.')]
[switch]$UpdateDynamicGroupQueries=$false,
[Parameter(Mandatory=$false, HelpMessage='Use this switch to keep Azure AD and Graph sessions open after execution.')]
[switch]$NoDisconnect=$false
)
# Sanitize input
if ($SearchFor.Length -lt 4) { Throw "You are searching for less than 4 characters? That can't be right."; }
if ($ReplaceWith.Length -lt 4) { Throw "You are replacing with less than 4 characters? That can't be right."; }
$Operator = $Operator.ToLower();
if ($Operator -notin @('equals','startswith','endswith','contains')) {
Throw "The '$($Operator)' is not supported. Please use 'equals','startswith','endswith' or 'contains'.";
}
if ($UpdateDynamicGroupQueries -and (-not($Operator -in 'equals','startswith'))) {
Throw "You cannot use -UpdateDynamicGroupQueries with an -Operator other than 'startwith' or 'equal'.";
}
# Import required modules.
if (Get-InstalledModule -Name 'AzureADPreview') {
Import-Module -Name 'AzureADPreview'; # This is only for my personal convenience; the AzureAD module will do :).
} else {
Import-Module -Name 'AzureAD';
}
Import-Module -Name 'Microsoft.Graph.Authentication';
# Connect to Graph (if needed).
$graphContext = Get-MgContext -ErrorAction SilentlyContinue;
if (-not($graphContext)) {
# No Graph context means no valid Graph session.
Connect-MgGraph | Out-Null;
Select-MgProfile -Name 'beta';
}
# Connect to AzureAD (if needed).
Try {
Get-AzureADCurrentSessionInfo -ErrorAction SilentlyContinue | Out-Null;
}
Catch {
# An exception from Get-AzureADCurrentSessionInfo indicates no valid AAD session.
# It will always throw an exception in that case, even when instructed not to by the -EA. Hence the Try/Catch.
Connect-AzureAD | Out-Null;
}
# Find all devices with matching group tags and replace $SearchFor with $ReplaceWith.
$apDevices = Get-AutopilotDevices -GroupTag $SearchFor -Operator $Operator;
foreach($apDevice in $apDevices) {
$groupTagToSet = $apDevice.groupTag.Replace($SearchFor, $ReplaceWith);
Set-AutopilotGroupTag -AutopilotDeviceId $apDevice.id -GroupTag $groupTagToSet;
}
# Find all dynamic group membership queries referencing this the group tag.
if ($UpdateDynamicGroupQueries) {
# First fetch all dynamic groups.
$aadGroups = Get-AzureADMSGroup -Filter "GroupTypes/any(s:s eq 'DynamicMembership')";
foreach($aadGroup in $aadGroups) {
# Evaluate if the query references the group tag. Note the escaping of the escape symbol. Awesomeness.
if ($aadGroup.MembershipRule -like "*``[OrderID``]:$($SearchFor)*") {
if ($PSCmdlet.ShouldProcess($aadGroup.DisplayName, 'Replace group tag in group''s membership rule(s)')) {
$newMembershipRule = $aadGroup.MembershipRule.Replace($SearchFor, $ReplaceWith);
Set-AzureADMSGroup -Id $aadGroup.Id -MembershipRule $newMembershipRule;
}
}
}
}
if (-not($NoDisconnect)) {
Disconnect-MgGraph;
Disconnect-AzureAD;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment