Public/Copy-SecurityPolicyToGPO.ps1

function Copy-SecurityPolicyToGPO {
    <#
    .SYNOPSIS
        Copies local security policy settings into a domain GPO. Writes to GPO only, never touches local policy.
 
    .DESCRIPTION
        Reads security policy settings from a source server using Get-LocalSecurityPolicy,
        then generates a GptTmpl.inf file and copies it into the GPO's SYSVOL path. This
        effectively applies the local security settings to the GPO.
 
        This function ONLY WRITES to the specified GPO. It NEVER modifies, deletes,
        or changes any local security policy setting on the source server.
 
        All write operations support -WhatIf so you can preview the migration.
 
    .PARAMETER SourceComputer
        The server to read local security policy from.
 
    .PARAMETER GPOName
        Name of the GPO to write the security policy settings into.
 
    .PARAMETER CreateGPO
        If specified, creates the GPO if it does not already exist.
 
    .PARAMETER Categories
        Which security policy categories to migrate. Valid values:
        SystemAccess, AuditPolicy, UserRights, SecurityOptions, All.
        Defaults to All.
 
    .PARAMETER WhatIf
        Shows what would happen without making any changes.
 
    .PARAMETER Confirm
        Prompts for confirmation before writing to the GPO.
 
    .EXAMPLE
        Copy-SecurityPolicyToGPO -SourceComputer "SVR-WEB-01" -GPOName "Security-WebServers" -Categories AuditPolicy,UserRights -CreateGPO
 
        Migrates audit policy and user rights settings from SVR-WEB-01 into a new GPO.
 
    .EXAMPLE
        Copy-SecurityPolicyToGPO -SourceComputer "SVR-WEB-01" -GPOName "Security-WebServers" -CreateGPO -WhatIf
 
        Previews the migration of all security policy categories.
 
    .OUTPUTS
        PSCustomObject with properties: SourceComputer, GPOName, SettingsRead, SettingsMigrated, Categories, Timestamp
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourceComputer,

        [Parameter(Mandatory = $true)]
        [string]$GPOName,

        [Parameter()]
        [switch]$CreateGPO,

        [Parameter()]
        [ValidateSet('SystemAccess', 'AuditPolicy', 'UserRights', 'SecurityOptions', 'All')]
        [string[]]$Categories = @('All'),

        [Parameter()]
        [switch]$Force
    )

    begin {
        Write-Verbose "Copy-SecurityPolicyToGPO: READ from '$SourceComputer', WRITE to GPO '$GPOName'."
        Write-Verbose "Local policy on '$SourceComputer' will NOT be modified."

        $timestamp        = Get-Date -Format 'yyyy-MM-dd'
        $settingsMigrated = 0

        # Map friendly category names to .inf section headers
        $categoryToSection = @{
            'SystemAccess'    = 'System Access'
            'AuditPolicy'     = 'Event Audit'
            'UserRights'      = 'Privilege Rights'
            'SecurityOptions' = 'Registry Values'
        }
    }

    process {
        # ------------------------------------------------------------------
        # Step 1: Read local security policy from the source server
        # ------------------------------------------------------------------
        Write-Verbose "Step 1: Reading local security policy from '$SourceComputer'..."
        $settings = Get-LocalSecurityPolicy -ComputerName $SourceComputer
        $settingsRead = ($settings | Measure-Object).Count

        if ($settingsRead -eq 0) {
            Write-Warning "No security policy settings found on '$SourceComputer'. Nothing to migrate."
            return [PSCustomObject]@{
                SourceComputer   = $SourceComputer
                GPOName          = $GPOName
                SettingsRead     = 0
                SettingsMigrated = 0
                Categories       = $Categories
                Timestamp        = $timestamp
            }
        }

        Write-Verbose "Found $settingsRead security policy setting(s)."

        # Filter by categories
        if ($Categories -notcontains 'All') {
            $settings = $settings | Where-Object { $Categories -contains $_.Category }
            Write-Verbose "Filtered to $($settings.Count) setting(s) in categories: $($Categories -join ', ')"
        }

        $filteredCategories = ($settings | Select-Object -ExpandProperty Category -Unique)

        if (-not $settings -or ($settings | Measure-Object).Count -eq 0) {
            Write-Warning "No settings match the specified categories."
            return
        }

        # ------------------------------------------------------------------
        # Step 2: Create or validate the GPO
        # ------------------------------------------------------------------
        Write-Verbose "Step 2: Preparing target GPO '$GPOName'..."

        $gpo = $null
        try {
            $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue
        }
        catch {
            # GPO does not exist
        }

        if (-not $gpo) {
            if ($CreateGPO) {
                if ($PSCmdlet.ShouldProcess($GPOName, 'Create new Group Policy Object')) {
                    try {
                        $gpo = New-GPO -Name $GPOName -Comment "Security policy migrated from $SourceComputer on $timestamp. Created by LocalPolicy-ToGPO module." -ErrorAction Stop
                        Write-Verbose "Created GPO '$GPOName'."
                    }
                    catch {
                        Write-Error "Failed to create GPO '$GPOName': $_"
                        return
                    }
                }
                else {
                    Write-Verbose "GPO creation skipped (WhatIf mode)."
                }
            }
            else {
                Write-Error "GPO '$GPOName' does not exist. Use -CreateGPO to create it automatically."
                return
            }
        }
        else {
            Write-Verbose "GPO '$GPOName' already exists."
        }

        # ------------------------------------------------------------------
        # Step 3: Build the GptTmpl.inf content
        # ------------------------------------------------------------------
        Write-Verbose "Step 3: Generating GptTmpl.inf content..."

        $infBuilder = [System.Text.StringBuilder]::new()
        [void]$infBuilder.AppendLine('[Unicode]')
        [void]$infBuilder.AppendLine('Unicode=yes')
        [void]$infBuilder.AppendLine("[Version]")
        [void]$infBuilder.AppendLine('signature="$CHICAGO$"')
        [void]$infBuilder.AppendLine("Revision=1")
        [void]$infBuilder.AppendLine("; Migrated from $SourceComputer on $timestamp by LocalPolicy-ToGPO module")
        [void]$infBuilder.AppendLine()

        # Group settings by their .inf section
        foreach ($category in $filteredCategories) {
            $sectionName = $categoryToSection[$category]
            if (-not $sectionName) { continue }

            $sectionSettings = $settings | Where-Object { $_.Category -eq $category }

            [void]$infBuilder.AppendLine("[$sectionName]")

            foreach ($setting in $sectionSettings) {
                [void]$infBuilder.AppendLine("$($setting.SettingName) = $($setting.SettingValue)")
                $settingsMigrated++
            }

            [void]$infBuilder.AppendLine()
        }

        $infContent = $infBuilder.ToString()
        Write-Verbose "Generated GptTmpl.inf with $settingsMigrated setting(s)."

        # ------------------------------------------------------------------
        # Step 4: Write GptTmpl.inf to the GPO SYSVOL path
        # ------------------------------------------------------------------
        Write-Verbose "Step 4: Writing GptTmpl.inf to GPO SYSVOL path..."

        if ($gpo) {
            $gpoId   = $gpo.Id.ToString('B').ToUpper()
            $domainName = $null
            try {
                $domainName = (Get-ADDomain -ErrorAction Stop).DNSRoot
            }
            catch {
                try {
                    $domainName = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
                }
                catch {
                    Write-Error "Unable to determine domain name."
                    return
                }
            }

            $gpoPath = "\\$domainName\SYSVOL\$domainName\Policies\$gpoId\Machine\Microsoft\Windows NT\SecEdit"

            if ($PSCmdlet.ShouldProcess("$gpoPath\GptTmpl.inf", 'Write security policy settings to GPO')) {
                try {
                    # Create the directory structure if needed
                    if (-not (Test-Path $gpoPath)) {
                        New-Item -ItemType Directory -Path $gpoPath -Force | Out-Null
                        Write-Verbose "Created directory: $gpoPath"
                    }

                    # Write the .inf file
                    $infFilePath = Join-Path $gpoPath 'GptTmpl.inf'
                    Set-Content -Path $infFilePath -Value $infContent -Encoding Unicode -Force
                    Write-Verbose "Wrote GptTmpl.inf to '$infFilePath'."

                    # ------------------------------------------------------------------
                    # Step 5: Update GPO version to force replication
                    # ------------------------------------------------------------------
                    Write-Verbose "Step 5: Updating GPO version to trigger replication..."

                    $gptIniPath = "\\$domainName\SYSVOL\$domainName\Policies\$gpoId\GPT.INI"
                    if (Test-Path $gptIniPath) {
                        $gptContent = Get-Content -Path $gptIniPath -Raw
                        if ($gptContent -match 'Version=(\d+)') {
                            $currentVersion = [int]$Matches[1]
                            # Increment the machine portion (lower 16 bits)
                            $newVersion = $currentVersion + 1
                            $newGptContent = $gptContent -replace "Version=\d+", "Version=$newVersion"
                            Set-Content -Path $gptIniPath -Value $newGptContent -Encoding ASCII -Force
                            Write-Verbose "Updated GPT.INI version from $currentVersion to $newVersion."
                        }
                    }

                    # Also update the AD version attribute
                    try {
                        $gpo.MakeAclConsistent()
                    }
                    catch {
                        Write-Verbose "Could not update AD GPO version (non-critical): $_"
                    }
                }
                catch {
                    Write-Error "Failed to write GptTmpl.inf to GPO path: $_"
                    return
                }
            }
        }
        else {
            Write-Verbose "GPO object not available (WhatIf mode). Would write $settingsMigrated settings to GptTmpl.inf."
        }
    }

    end {
        $summary = [PSCustomObject]@{
            SourceComputer   = $SourceComputer
            GPOName          = $GPOName
            SettingsRead     = $settingsRead
            SettingsMigrated = $settingsMigrated
            Categories       = $filteredCategories -join ', '
            Timestamp        = $timestamp
        }

        Write-Verbose "Migration complete: $settingsRead read, $settingsMigrated migrated."
        $summary
    }
}