Private/Update-GpoVersion.ps1

function Update-GpoVersion {

    <#
        .SYNOPSIS
            Updates the version number of specified Group Policy Objects (GPOs).

        .DESCRIPTION
            This function increments the computer version number of specified Group Policy Objects (GPOs).

            When Group Policy settings are modified programmatically (without using the Group Policy
            Management Console), the version numbers need to be manually updated to ensure the changes
            are properly applied to domain computers and users. This function handles that process by:

            - Updating the version number in the Active Directory GPO object
            - Updating the version number in the GPT.INI file in the SYSVOL share
            - Supporting both single GPO updates and batch processing via pipeline
            - Providing proper error handling and validation

            By default, the function increments the version by 3 (1 for user settings, 2 for computer
            settings) to ensure both parts are refreshed, but this can be customized.

        .PARAMETER GpoName
            The name of the Group Policy Object to update. This parameter accepts pipeline input,
            allowing for batch processing of multiple GPOs.

        .PARAMETER IncrementBy
            The number to increment the version by. Defaults to 3, which ensures both user (1)
            and computer (2) settings are refreshed. Valid values range from 1 to 100.

        .EXAMPLE
            Update-GpoVersion -GpoName "Default Domain Policy"

            Updates the version number of the "Default Domain Policy" GPO, incrementing it by 3.

        .EXAMPLE
            Get-GPO -All | Where-Object {$_.DisplayName -like "*Security*"} | Update-GpoVersion

            Updates version numbers for all GPOs with "Security" in their name, processing them
            via the pipeline.

        .EXAMPLE
            Update-GpoVersion -GpoName "Custom Security Policy" -IncrementBy 1

            Updates only the user settings version number for the specified GPO.

        .INPUTS
            System.String
            Microsoft.GroupPolicy.Gpo

            You can pipe GPO names as strings or GPO objects from Get-GPO to this function.

        .OUTPUTS
            System.Void

            This function does not generate any output. It modifies GPO version numbers directly.

        .NOTES
            Used Functions:
                Name ║ Module/Namespace
                ═══════════════════════════════════════════╬══════════════════════════════
                Get-GPO ║ GroupPolicy
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Get-FunctionDisplay ║ EguibarIT.DelegationPS
                Import-MyModule ║ EguibarIT.DelegationPS

        .NOTES
            Version: 2.0
            DateModified: 22/May/2025
            LastModifiedBy: Vicente Rodriguez Eguibar
                            vicente@eguibar.com
                            Eguibar IT
                            http://www.eguibarit.com

        .LINK
            https://github.com/vreguibar/EguibarIT.DelegationPS

        .LINK
            https://learn.microsoft.com/en-us/powershell/module/groupolicy/get-gpo

        .COMPONENT
            Group Policy

        .ROLE
            Security Administration

        .FUNCTIONALITY
            Group Policy Management
    #>


    [CmdletBinding(SupportsShouldProcess = $true,
        ConfirmImpact = 'Medium')]
    [OutputType([void])]

    param (

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Specify the name of the GPO.',
            Position = 0)]
        [ValidateNotNullOrEmpty()]
        [Alias('Name', 'DisplayName')]
        [string]
        $GpoName,

        [Parameter(Mandatory = $false,
            HelpMessage = 'Specify the increment number.',
            Position = 2)]
        [ValidateRange(1, 100)]
        [PSDefaultValue(Help = 'Default Value is "3"')]
        [int]
        $IncrementBy = 3

    )

    Begin {

        Set-StrictMode -Version Latest

        # Display function header if variables exist
        if ($null -ne $Variables -and $null -ne $Variables.HeaderDelegation) {
            $txt = ($Variables.HeaderDelegation -f
                (Get-Date).ToString('dd/MMM/yyyy'),
                $MyInvocation.Mycommand,
                (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False)
            )
            Write-Verbose -Message $txt
        } #end if

        ##############################
        # Module imports
        Import-MyModule -Name 'GroupPolicy' -SkipEditionCheck -Verbose:$false

        ##############################
        # Variables Definition
        [Int64]$versionObject = $null

        # Retrieve the GPO object by name
        $gpo = Get-GPO -Name $PsBoundParameters['GpoName'] -ErrorAction Stop

        # Get the GPO ID
        $gpoId = ('{' + $gpo.Id + '}')

        # Build SYSVOL path
        $sysVolPath = '\\{0}\SYSVOL\{0}' -f $env:USERDNSDOMAIN
        $pathToGpt = '{0}\Policies\{1}\gpt.ini' -f $sysVolPath, $gpoId

        Write-Debug -Message ('Path to GPT: {0}' -f $pathToGpt)

    } #end Begin

    Process {

        Try {

            # Get the GPO object
            $url = 'LDAP://CN={0},CN=Policies,CN=System,{1}' -f $gpoId, $Variables.defaultNamingContext
            $de = [System.DirectoryServices.DirectoryEntry]::New($url)

            Write-Debug -Message ('Accessing GPO through DirectoryEntry: {0}' -f $url)

        } catch {

            Write-Error -Message ('Error accessing GPO through DirectoryEntry' -f $Gpo.Name)

        } #end Try-Catch

        # Get the VersionObject of the DirectoryEntry (the GPO)
        $versionObject = [Int64]($de.Properties['VersionNumber'].Value.ToString())

        # Convert the value into a 8 digit HEX string
        $hexValue = $versionObject.ToString('x8')

        # Top 16 bits HEX UserVersionNumber - first 4 characters (complete with zero to the left)
        # This is the UserVersion
        $hexUserVN = $hexValue.Substring(0, 4)

        # Lower 16 bits HEX ComputerVersionNumber - last 4 characters (complete with zero to the left)
        # This is the ComputerVersion
        $hexComputerVN = $hexValue.Substring(4)

        # Lower 16 bits as Integer ComputerVersionNumber
        $computerVN = [Convert]::ToInt64($hexComputerVN, 16)

        # Use IncrementBy parameter
        $computerVN += $IncrementBy

        # Concatenate '0x' and 'HEX UserVersionNumber having 4 digits' and 'HEX ComputerVersionNumber having 4 digits'
        $newHex = '0x{0}{1}' -f $hexUserVN, $computerVN.ToString('x4')

        # Convert the New Hex number to integer
        $newVersionObject = [Convert]::ToInt64($newHex, 16)


        try {

            if ($PSCmdlet.ShouldProcess($GpoName, 'Update GPO version')) {

                # Update the GPO VersionNumber with the new value
                $de.Properties['VersionNumber'].Value = $newVersionObject.ToString()

                # Last, write the GPCMachineExtensionName attribute with the Client-Side Extension GUID
                # If not the settings won't display in the GPO Management tool and the target
                # server won't be able to read the GPO.
                $de.Properties['gPCMachineExtensionNames'].Value = '[{827D319E-6EAC-11D2-A4EA-00C04F79F83A}{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}]'

                # Save the information on the DirectoryObject
                $de.CommitChanges()

                # Close the DirectoryEntry
                $de.Close()

                Write-Debug -Message ('Old GPO Version Number: {0}' -f $versionObject)
                Write-Debug -Message ('New GPO Version Number: {0}' -f $newVersionObject)

                # Write new version value to GPT (Including Section Name). Update SYSVOL file
                if (Test-Path -Path $pathToGpt) {

                    try {
                        # New instance of IniFile class
                        $Gpt = [IniFileHandler.IniFile]::new($pathToGpt)

                        # Check section exists
                        if ($Gpt.SectionExists('General')) {

                            Write-Debug -Message ('Section Name: General')

                            # Change value of an existing key
                            $Gpt.SetKeyValue('General', 'Version', $newVersionObject.ToString())
                            $Gpt.SetKeyValue('General', 'displayName', $Gpo.DisplayName)

                        } else {

                            Write-Debug -Message 'Section [General] does not exist. Creating it with Key=Value.'

                            $Gpt.AddSection('General')

                            # Add a new Key/Value pair within a given section
                            $Gpt.SetKeyValue('General', 'Version', $newVersionObject.ToString())
                            $Gpt.SetKeyValue('General', 'displayName', $Gpo.DisplayName)

                        } #end If-Else

                        # Save file using default encoding UTF-8
                        $Gpt.SaveFile($pathToGpt)

                        Write-Debug -Message ('Saving new Version of GPO {0} to file {1}' -f $Gpo.DisplayName, $pathToGpt)

                    } catch {

                        Write-Error -Message ('Failed to update GPT.INI for {0}: {1}' -f $Gpo.DisplayName, $_.Exception.Message)
                        continue

                    } #end Try-Catch

                } #end If

            } #end If

        } catch {

            throw "The GPTs.ini file could not be modified: $_. Message is $($_.Exception.Message)"

        } #end Try-Catch

    } #end Process

    End {

        if ($null -ne $Variables -and $null -ne $Variables.FooterDelegation) {
            $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName,
                'Version of GPO updated (Private Function).'
            )
            Write-Verbose -Message $txt
        } #end if

    } #end End
} #end function Update-GpoVersion