I was able to update some test groups successfully with the following script
<#
.SYNOPSIS
Updates the scope of QUERY and CONTAINER membership rules from
OU=old,DC=domain,DC=tld to DC=domain,DC=tld.
.DESCRIPTION
GUID ENDIANNESS NOTE:
IADs.GUID returns a hex string with the first three components byte-swapped (little-endian).
Adaxes stores scope paths using the standard wire-format GUID, e.g. "17894c9b-...".
This script uses Convert-AdaxesGuid to produce the correct wire-format GUID string
for both the old and new scope, so comparisons and writes are correct.
The script:
1. Resolves both scope DNs to their wire-format GUID paths at startup.
2. For each QUERY/CONTAINER rule, checks for an exact match against the old scope path.
3. Replaces matching paths with the new scope path.
4. Calls SetInfo() + UpdateMembershipNow() on changed groups.
SAFE BY DEFAULT: $whatIfMode = $true — no changes until set to $false.
.PARAMETERS
None — configure via #region Configuration below.
.OUTPUTS
Console log + CSV change report at $reportPath.
.EXAMPLE
# Dry-run (default):
.\Set-RuleBasedGroupScope.ps1
# Live run — set $whatIfMode = $false first:
.\Set-RuleBasedGroupScope.ps1
.NOTES
Author = wintec01
Date = 2026-05-19
Version = 5.0
Depends = none
#>
[Reflection.Assembly]::LoadWithPartialName("Softerra.Adaxes.Adsi") | Out-Null
#region Safety controls
$debugMode = $false
$whatIfMode = $true # ← Set to $false to apply real changes
#endregion
#region Configuration
$oldScopeDN = "OU=old,DC=domain,DC=tld"
$newScopeDN = "DC=domain,DC=tld"
$reportPath = "C:\Temp\RuleBasedGroups_ScopeUpdate_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
#endregion
#region Helper: Convert IADs.GUID (little-endian hex) to wire-format GUID string
function Convert-AdaxesGuid {
param([string]$adsiHexGuid)
$bytes = [byte[]]::new(16)
for ($i = 0; $i -lt 16; $i++) {
$bytes[$i] = [Convert]::ToByte($adsiHexGuid.Substring($i * 2, 2), 16)
}
return [System.Guid]::new($bytes).ToString()
}
#endregion
#region Helper logging
# Define logging function
function Write-LogV2 {
<#
.SYNOPSIS
Writes log messages with different severity levels
.DESCRIPTION
Writes formatted log messages to Adaxes context or console with color coding
.PARAMETER Level
Log level: INFO, ERROR, DEBUG, VERBOSE, WARNING
.PARAMETER Message
The message to log
#>
param (
[string]$Level = "INFO",
[string]$Message
)
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$logEntry = "$timestamp [$Level] $Message"
if ($null -ne $Context) {
switch($Level) {
"INFO" {
$logLevel = "Information"
}
{ $_ -eq "ERR" -or $_ -eq "ERROR" } {
$logLevel = "Error"
}
{ $_ -eq "DBG" -or $_ -eq "DEBUG" } {
$logLevel = "Information"
}
{ $_ -eq "VER" -or $_ -eq "VERB" -or $_ -eq "VERBOSE"} {
$logLevel = "Information"
}
{ $_ -eq "WARN" -or $_ -eq "WARNING" } {
$logLevel = "Warning"
}
default {
$logLevel = "Information"
}
}
$Context.LogMessage($logEntry, $logLevel)
} else {
$color = switch ($Level) {
"INFO" {
"Green"
}
{ $_ -eq "ERR" -or $_ -eq "ERROR" } {
"Red"
}
{ $_ -eq "DBG" -or $_ -eq "DEBUG" } {
"Cyan"
}
{ $_ -eq "VER" -or $_ -eq "VERB" -or $_ -eq "VERBOSE"} {
"Cyan"
}
{ $_ -eq "WARN" -or $_ -eq "WARNING" } {
"Yellow"
}
default {
"White"
}
}
Write-Host $logEntry -ForegroundColor $color
}
}
# Debug logging function
function Debug-LogV2 {
<#
.SYNOPSIS
Writes debug log messages conditionally
.PARAMETER debug
Boolean flag to enable debug logging
.PARAMETER Message
The debug message to log
#>
param (
[bool]$Enabled,
[string]$Message
)
if ($Enabled) {
Write-LogV2 -Level "DBG" -Message $Message
}
}
# Verbose logging function
function Verbose-LogV2 {
<#
.SYNOPSIS
Writes verbose log messages conditionally
.PARAMETER verbose
Boolean flag to enable verbose logging
.PARAMETER Message
The verbose message to log
#>
param (
[boolean]$verbose,
[string]$Message
)
if ($verbose) {
Write-LogV2 -Level "VERB" -Message $Message
}
}
#endregion
#region Main
Write-LogV2 -Level "INFO" -Message "=== Rule-Based Group Scope Remediation started ==="
Write-LogV2 -Level "INFO" -Message "WhatIf mode : $($whatIfMode)"
$ns = New-Object "Softerra.Adaxes.Adsi.AdmNamespace"
$service = $ns.GetServiceDirectly("localhost")
# Step 1: Resolve both scope DNs to wire-format GUID paths.
Write-LogV2 -Level "INFO" -Message "Resolving scope DNs to wire-format GUID paths..."
try {
$oldScopeObj = $service.OpenObject("Adaxes://$oldScopeDN", $null, $null, 0)
$oldScopeGuidWire = Convert-AdaxesGuid $oldScopeObj.GUID
$oldScopePath = "Adaxes://<GUID=$oldScopeGuidWire>"
Write-LogV2 -Level "INFO" -Message "Old scope : $($oldScopeDN) → $($oldScopePath)"
} catch {
Write-LogV2 -Level "ERR" -Message "Cannot resolve old scope '$($oldScopeDN)': $($_.Exception.Message)"
exit 1
}
try {
$newScopeObj = $service.OpenObject("Adaxes://$newScopeDN", $null, $null, 0)
$newScopeGuidWire = Convert-AdaxesGuid $newScopeObj.GUID
$newScopePath = "Adaxes://<GUID=$newScopeGuidWire>"
Write-LogV2 -Level "INFO" -Message "New scope : $($newScopeDN) → $($newScopePath)"
} catch {
Write-LogV2 -Level "ERR" -Message "Cannot resolve new scope '$($newScopeDN)': $($_.Exception.Message)"
exit 1
}
if ($oldScopePath -eq $newScopePath) {
Write-LogV2 -Level "ERR" -Message "Old and new scope resolve to the same GUID path — nothing to do. Exiting."
exit 1
}
$report = [System.Collections.Generic.List[PSObject]]::new()
$groupsUpdated = 0
$rulesUpdated = 0
$groupsWithErrors = 0
try {
$queries = $service.GetServiceObject("ADM_SERVICEOBJECTID_RULEBASEDGROUPQUERIES")
$ruleBasedGroupGuids = $queries.GetRuleBasedGroups()
$groupCount = ($ruleBasedGroupGuids | Measure-Object).Count
Write-LogV2 -Level "INFO" -Message "Rule-based groups to process: $($groupCount)"
foreach ($guid in $ruleBasedGroupGuids) {
try {
$group = $service.OpenObject("Adaxes://<GUID=$guid>", $null, $null, 0)
$groupDN = $group.Get("distinguishedName")
$rules = $group.MembershipRules
$groupChanged = $false
$ruleIndex = 0
foreach ($rule in $rules) {
$ruleType = $rule.Type
switch ($ruleType) {
"ADM_BUSINESSUNITMEMBERSHIPTYPE_QUERY" {
$currentPath = $rule.BaseObjectPath
if ([string]::IsNullOrEmpty($currentPath)) {
# No container scope — criteria-only rule, nothing to update
break
}
if ($currentPath -ne $oldScopePath) {
Debug-LogV2 -Enabled $debugMode -Message " Rule[$($ruleIndex)] QUERY scope='$($currentPath)' — no change needed"
break
}
if ($whatIfMode) {
Write-LogV2 -Level "INFO" -Message " WHATIF Rule[$($ruleIndex)] QUERY on '$($groupDN)'"
Write-LogV2 -Level "INFO" -Message " '$($currentPath)' → '$($newScopePath)'"
} else {
$rule.BaseObjectPath = $newScopePath
Write-LogV2 -Level "INFO" -Message " UPDATED Rule[$($ruleIndex)] QUERY on '$($groupDN)'"
Write-LogV2 -Level "INFO" -Message " '$($currentPath)' → '$($newScopePath)'"
$rulesUpdated++
$groupChanged = $true
}
$report.Add([PSCustomObject]@{
GroupGUID = $guid; GroupDN = $groupDN; RuleIndex = $ruleIndex
RuleType = $ruleType; OldPath = $currentPath
NewPath = if ($whatIfMode) { "(WHATIF) $($newScopePath)" } else { $newScopePath }
Status = if ($whatIfMode) { "WHATIF" } else { "UPDATED" }
})
}
"ADM_BUSINESSUNITMEMBERSHIPTYPE_CONTAINER" {
$currentPath = $rule.ContainerPath
if ([string]::IsNullOrEmpty($currentPath)) {
break
}
if ($currentPath -ne $oldScopePath) {
Debug-LogV2 -Enabled $debugMode -Message " Rule[$($ruleIndex)] CONTAINER scope='$($currentPath)' — no change needed"
break
}
if ($whatIfMode) {
Write-LogV2 -Level "INFO" -Message " WHATIF Rule[$($ruleIndex)] CONTAINER on '$($groupDN)'"
Write-LogV2 -Level "INFO" -Message " '$($currentPath)' → '$($newScopePath)'"
} else {
$rule.ContainerPath = $newScopePath
Write-LogV2 -Level "INFO" -Message " UPDATED Rule[$($ruleIndex)] CONTAINER on '$($groupDN)'"
Write-LogV2 -Level "INFO" -Message " '$($currentPath)' → '$($newScopePath)'"
$rulesUpdated++
$groupChanged = $true
}
$report.Add([PSCustomObject]@{
GroupGUID = $guid; GroupDN = $groupDN; RuleIndex = $ruleIndex
RuleType = $ruleType; OldPath = $currentPath
NewPath = if ($whatIfMode) { "(WHATIF) $($newScopePath)" } else { $newScopePath }
Status = if ($whatIfMode) { "WHATIF" } else { "UPDATED" }
})
}
default {
Debug-LogV2 -Enabled $debugMode -Message " Rule[$($ruleIndex)] $($ruleType) — no scope, skipping"
}
}
$ruleIndex++
}
if ($groupChanged) {
try {
$group.MembershipRules = $rules
$group.SetInfo()
$group.UpdateMembershipNow()
$groupsUpdated++
Write-LogV2 -Level "INFO" -Message " Saved + UpdateMembershipNow() on '$($groupDN)'"
} catch {
$groupsWithErrors++
Write-LogV2 -Level "ERR" -Message " SetInfo/UpdateMembershipNow failed on '$($groupDN)': $($_.Exception.Message)"
}
}
} catch {
$groupsWithErrors++
Write-LogV2 -Level "ERR" -Message "Failed to process GUID '$($guid)': $($_.Exception.Message)"
$report.Add([PSCustomObject]@{
GroupGUID = $guid; GroupDN = "ERROR"; RuleIndex = "ERROR"
RuleType = "ERROR"; OldPath = "N/A"; NewPath = "N/A"
Status = "ERROR: $($_.Exception.Message)"
})
}
}
if (($report | Measure-Object).Count -gt 0) {
$report | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
Write-LogV2 -Level "INFO" -Message "Report written to: $($reportPath)"
} else {
Write-LogV2 -Level "INFO" -Message "No matching rules found — nothing to report."
}
} catch {
Write-LogV2 -Level "ERR" -Message "Unhandled error: $($_.Exception.Message)"
}
Write-LogV2 -Level "INFO" -Message "=== Summary ==="
Write-LogV2 -Level "INFO" -Message "WhatIf mode : $($whatIfMode)"
Write-LogV2 -Level "INFO" -Message "Old scope path : $($oldScopePath)"
Write-LogV2 -Level "INFO" -Message "New scope path : $($newScopePath)"
Write-LogV2 -Level "INFO" -Message "Groups updated : $($groupsUpdated)"
Write-LogV2 -Level "INFO" -Message "Rules updated : $($rulesUpdated)"
Write-LogV2 -Level "INFO" -Message "Groups with errors : $($groupsWithErrors)"
Write-LogV2 -Level "INFO" -Message "=== Remediation completed ==="
#endregion