Hi team,

Due to some restructuring in our native AD, we need to update our rule-based groups - specifically the scope of the rules, which need to be changed from something like OU=old,DC=domain,DC=tld to DC=domain,DC=tld, or to include a new OU in the rule, such as OU=new,DC=domain,DC=tld.

Do you have anything in place for bulk updating these, or a script to manage this? I already looked at the SDKs but could not find the best approach.

Thanks

ago by (2.5k points)

1 Answer

ago by (309k points)
0 votes

Hello,

Unfortunately, there is nothing like that. However, the following article might be helpful: https://adaxes.com/sdk/IAdmGroup2.

ago by (2.5k points)
0

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

Related questions

I have found the script to force membership updates for all rule based groups, but is there a script to force update a specific rule based group? I am looking to add a ... I would like to trigger a rule based group that adds members of the manual group. Thanks

asked Jul 9, 2025 by msheppard (880 points)
0 votes
1 answer

Hi team, is it somehow possible to fetch and export information from scheduled tasks and rule based groups about their schedule time? Maybe also about the next run time and how ... an overview and see if some or too many tasks are running at the same time.

asked Mar 5, 2025 by wintec01 (2.5k points)
0 votes
1 answer

I have the need to run a scheduled task that executes a PowerShell script to update the user criteria item on a rule-based group. I have a good start on the script, but ... "Seasonal" -or employeeType -eq "Elected" -or employeeType -eq "State"}). #&gt; }

asked Sep 27, 2024 by emeisner (210 points)
0 votes
1 answer

When setting up a rule based group, GMSA objects are not visible. Is there a setting or view I need to add to make these availabe to rule based groups, or is it simply not an option?

asked Sep 16, 2024 by ajmilic (130 points)
0 votes
1 answer

Hi, would it be possible to achieve the following idea: Creating and updating rule based groups, based on user attributes like company? For each company value in AD, ... get all unique company values, then create a group with this company value as filter.

asked Mar 7, 2024 by wintec01 (2.5k points)
0 votes
1 answer