Public/Copy-FirewallToGPO.ps1

function Copy-FirewallToGPO {
    <#
    .SYNOPSIS
        Copies local firewall rules into a domain GPO. Writes to GPO only, never touches local policy.
 
    .DESCRIPTION
        Reads firewall rules from a source server using Get-LocalFirewallPolicy, then
        creates equivalent rules in a domain GPO using New-NetFirewallRule with
        the -PolicyStore parameter targeting the GPO.
 
        This function ONLY WRITES to the specified GPO. It NEVER modifies, deletes,
        or changes any local firewall rule on the source server. The source is read-only.
 
        Every New-NetFirewallRule call supports -WhatIf so you can preview the migration
        before committing. Rule descriptions are prefixed with migration metadata for
        audit trail purposes.
 
    .PARAMETER SourceComputer
        The server to read local firewall rules from.
 
    .PARAMETER GPOName
        Name of the GPO to create the firewall rules in. If -CreateGPO is specified and
        GPOName is not provided, the name defaults to "Firewall - ServerName - Migrated YYYY-MM-DD".
 
    .PARAMETER CreateGPO
        If specified, creates the GPO if it does not already exist.
 
    .PARAMETER ProfileFilter
        Filter source rules by firewall profile. Valid values: Domain, Private, Public, Any.
        Defaults to Any.
 
    .PARAMETER EnabledOnly
        When specified (default), only migrates rules that are currently enabled.
 
    .PARAMETER WhatIf
        Shows what would happen without making any changes.
 
    .PARAMETER Confirm
        Prompts for confirmation before each rule is created in the GPO.
 
    .EXAMPLE
        Copy-FirewallToGPO -SourceComputer "SVR-WEB-01" -GPOName "Firewall-WebServers" -CreateGPO -WhatIf
 
        Previews the migration of all local firewall rules from SVR-WEB-01 into a new GPO.
 
    .EXAMPLE
        Copy-FirewallToGPO -SourceComputer "SVR-WEB-01" -GPOName "Firewall-WebServers" -CreateGPO -ProfileFilter Domain
 
        Migrates only Domain-profile firewall rules from SVR-WEB-01 into the GPO.
 
    .OUTPUTS
        PSCustomObject with properties: SourceComputer, GPOName, RulesRead, RulesMigrated, RulesFailed, Timestamp
    #>

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

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

        [Parameter()]
        [switch]$CreateGPO,

        [Parameter()]
        [ValidateSet('Domain', 'Private', 'Public', 'Any')]
        [string]$ProfileFilter = 'Any',

        [Parameter()]
        [switch]$EnabledOnly = $true
    )

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

        $timestamp    = Get-Date -Format 'yyyy-MM-dd'
        $rulesMigrated = 0
        $rulesFailed   = 0
    }

    process {
        # ------------------------------------------------------------------
        # Step 1: Read local firewall rules from the source server
        # ------------------------------------------------------------------
        Write-Verbose "Step 1: Reading local firewall rules from '$SourceComputer'..."
        $getParams = @{
            ComputerName  = $SourceComputer
            ProfileFilter = $ProfileFilter
            EnabledOnly   = $EnabledOnly
        }
        $rules = Get-LocalFirewallPolicy @getParams
        $rulesRead = ($rules | Measure-Object).Count

        if ($rulesRead -eq 0) {
            Write-Warning "No local firewall rules found on '$SourceComputer' matching the specified criteria. Nothing to migrate."
            return [PSCustomObject]@{
                SourceComputer = $SourceComputer
                GPOName        = $GPOName
                RulesRead      = 0
                RulesMigrated  = 0
                RulesFailed    = 0
                Timestamp      = $timestamp
            }
        }

        Write-Verbose "Found $rulesRead local firewall rule(s) to migrate."

        # ------------------------------------------------------------------
        # 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 "Firewall rules 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-Error "GPO '$GPOName' does not exist. Use -CreateGPO to create it automatically."
                return
            }
        }
        else {
            Write-Verbose "GPO '$GPOName' already exists."
        }

        # ------------------------------------------------------------------
        # Step 3: Determine the PolicyStore path for the GPO
        # ------------------------------------------------------------------
        $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. Ensure the computer is domain-joined and AD modules are available."
                return
            }
        }

        $policyStore = "$domainName\$GPOName"
        Write-Verbose "PolicyStore target: '$policyStore'"

        # ------------------------------------------------------------------
        # Step 4: Create each firewall rule in the GPO
        # ------------------------------------------------------------------
        Write-Verbose "Step 4: Migrating firewall rules to GPO..."

        foreach ($rule in $rules) {
            $migrationDesc = "[Migrated from $SourceComputer on $timestamp] $($rule.Description)"

            $ruleParams = @{
                PolicyStore  = $policyStore
                DisplayName  = $rule.DisplayName
                Direction    = $rule.Direction
                Action       = $rule.Action
                Profile      = $rule.Profile
                Enabled      = if ($rule.Enabled) { 'True' } else { 'False' }
                Description  = $migrationDesc
                ErrorAction  = 'Stop'
            }

            # Add optional parameters only when meaningful values exist
            if ($rule.Protocol -and $rule.Protocol -ne 'Any') {
                $ruleParams['Protocol'] = $rule.Protocol
            }
            if ($rule.LocalPort -and $rule.LocalPort -ne 'Any') {
                $ruleParams['LocalPort'] = $rule.LocalPort
            }
            if ($rule.RemotePort -and $rule.RemotePort -ne 'Any') {
                $ruleParams['RemotePort'] = $rule.RemotePort
            }
            if ($rule.LocalAddress -and $rule.LocalAddress -ne 'Any') {
                $ruleParams['LocalAddress'] = $rule.LocalAddress
            }
            if ($rule.RemoteAddress -and $rule.RemoteAddress -ne 'Any') {
                $ruleParams['RemoteAddress'] = $rule.RemoteAddress
            }
            if ($rule.Program) {
                $ruleParams['Program'] = $rule.Program
            }

            $whatIfMessage = "Create firewall rule '$($rule.DisplayName)' ($($rule.Direction)/$($rule.Action)) in GPO '$GPOName'"

            if ($PSCmdlet.ShouldProcess($whatIfMessage, 'New-NetFirewallRule')) {
                try {
                    New-NetFirewallRule @ruleParams | Out-Null
                    $rulesMigrated++
                    Write-Verbose "Migrated: $($rule.DisplayName) ($($rule.Direction)/$($rule.Action)/$($rule.Protocol):$($rule.LocalPort))"
                }
                catch {
                    $rulesFailed++
                    Write-Warning "Failed to migrate rule '$($rule.DisplayName)': $_"
                }
            }
        }
    }

    end {
        $summary = [PSCustomObject]@{
            SourceComputer = $SourceComputer
            GPOName        = $GPOName
            RulesRead      = $rulesRead
            RulesMigrated  = $rulesMigrated
            RulesFailed    = $rulesFailed
            Timestamp      = $timestamp
        }

        Write-Verbose "Migration complete: $rulesRead read, $rulesMigrated migrated, $rulesFailed failed."
        $summary
    }
}