GPOTools.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\GPOTools.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
#$script:doDotSource = Get-PSFConfigValue -FullName GPOTools.Import.DoDotSource -Fallback $false
$script:doDotSource = $false
if ($GPOTools_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
#$importIndividualFiles = Get-PSFConfigValue -FullName GPOTools.Import.IndividualFiles -Fallback $false
$importIndividualFiles = $false
if ($GPOTools_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path "$($script:ModuleRoot)\..\.git") { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'GPOTools' -Language 'en-US'

function ConvertFrom-ImportedIdentity
{
<#
    .SYNOPSIS
        Converts an imported identity into a security principal.
     
    .DESCRIPTION
        Converts an imported identity into a security principal.
        This is used for granting permissions.
     
    .PARAMETER Permission
        The permission object containing the source principal.
     
    .PARAMETER DomainObject
        An object representing the destination domain (as returned by Get-ADDomain)
     
    .EXAMPLE
        PS C:\> ConvertFrom-ImportedIdentity -Permission $permission -DomainObject $domainObject
     
        Resolves the source identity into a destination security principal.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [OutputType([System.Security.Principal.IdentityReference])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $Permission,
        
        [Parameter(Mandatory = $true)]
        $DomainObject
    )
    
    process
    {
        switch ($Permission.PrincipalType)
        {
            'Local BuiltIn' { return [System.Security.Principal.SecurityIdentifier]$Permission.SID }
            'foreignSecurityPrincipal' { return [System.Security.Principal.SecurityIdentifier]$Permission.SID }
            'group'
            {
                if ($Permission.IsBuiltIn -like 'true')
                {
                    return [System.Security.Principal.SecurityIdentifier]('{0}-{1}' -f $DomainObject.DomainSID, $Permission.RID)
                }
                else
                {
                    $identity = $script:identityMapping | Where-Object SID -EQ $Permission.SID
                    if (-not $identity) { throw "Cannot resolve $($Permission.IdentityReference) ($($Permission.SID))" }
                    return [System.Security.Principal.NTAccount]('{0}\{1}' -f $DomainObject.NetBIOSName, $identity.Target)
                }
            }
        }
    }
}

function ConvertTo-DnsDomainName
{
<#
    .SYNOPSIS
        Converts a distinguished name in the DNS domain name.
     
    .DESCRIPTION
        This extracts the domain portion of a distinguished name and processes it as dns name.
     
    .PARAMETER DistinguishedName
        The name to parse / convert.
     
    .EXAMPLE
        PS C:\> Get-ADDomain | ConvertTo-DnsDomainName
     
        Returns the dns name of the current domain.
#>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [Alias('Name')]
        [string[]]
        $DistinguishedName
    )
    
    process
    {
        foreach ($distName in $DistinguishedName)
        {
            ($distName -split "," | Where-Object { $_ -like "DC=*" } | ForEach-Object {
                $_ -replace '^DC='
            }) -join "."
        }
    }
}

function New-ImportResult
{
<#
    .SYNOPSIS
        Create unified import result objects.
     
    .DESCRIPTION
        Create unified import result objects.
     
    .PARAMETER Action
        The action taken.
     
    .PARAMETER Step
        The current step of the action.
     
    .PARAMETER Target
        The target of the step.
     
    .PARAMETER Success
        Whether the action was a success.
     
    .PARAMETER Data
        Any data to add to the report
     
    .PARAMETER ErrorData
        Any error data to add to the report
     
    .EXAMPLE
        PS C:\> New-ImportResult -Action 'Importing Policy Objects' -Step 'Import Object' -Target $gpoEntry -Success $true -Data $gpoEntry, $migrationTablePath
     
        Creates a new object representing a successful GPO import.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Action,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Step,
        
        $Target,
        
        [Parameter(Mandatory = $true)]
        [bool]
        $Success,
        
        $Data,
        
        $ErrorData
    )
    
    [pscustomobject]@{
        PSTypeName = 'GPOTools.ImportResult'
        Action       = $Action
        Step       = $Step
        Target       = $Target
        Success    = $Success
        Data       = $Data
        Error       = $ErrorData
    }
}

function New-MigrationTable
{
<#
    .SYNOPSIS
        Creates a new migration table used for GPO imports.
     
    .DESCRIPTION
        Creates a new migration table used for GPO imports.
        In this table, all source identities get matched to fitting destination identities.
        This ensures, that all identity references within GPOs remain intact.
     
    .PARAMETER Path
        The path where to spawn the migration table.
        Specify a folder, the file will be named '<DomainName>.migtable'
     
    .PARAMETER BackupPath
        The path where the GPO backups are stored.
     
    .PARAMETER Domain
        The domain the backup will be restored to.
        Defaults to the current user's domain.
     
    .EXAMPLE
        PS C:\> New-MigrationTable -Path '.' -BackupPath '.'
     
        Creates a migration table in the current path and looks in the current path for backup folders.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [Parameter(Mandatory = $true)]
        [string]
        $BackupPath,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $resolvedPath = (Resolve-Path $Path).ProviderPath
        $resolvedBackupPath = (Resolve-Path $BackupPath).ProviderPath
        $writePath = Join-Path -Path $resolvedPath -ChildPath "$Domain.migtable"
        
        #region Resolving source and destination Domain Names
        $domainObject = Get-ADDomain -Server $Domain
        $destDomainDNS = $domainObject.DNSRoot
        $destDomainNetBios = $domainObject.NetBIOSName
        
        if ($script:sourceDomainData)
        {
            $sourceDomainDNS = $script:sourceDomainData.DomainDNSName
            $sourceDomainNetBios = $script:sourceDomainData.NetBIOSName
        }
        elseif ($script:identityMapping.Count -gt 0)
        {
            $sourceDomainDNS = $script:identityMapping[0].DomainFqdn
            $sourceDomainNetBios = $script:identityMapping[0].DomainName
        }
        else
        {
            throw "Unable to determine source domain. Run Import-GptDomainData or Import-GptIdentity first!"
        }
        #endregion Resolving source and destination Domain Names
        
        #region Preparing imported identities
        $explicitIdentityMappings = foreach ($identity in $script:identityMapping)
        {
            if (($identity.IsBuiltIn -eq 'True') -and ($identity.SID -like "*-32-*"))
            {
                [PSCustomObject]@{
                    Source = $identity.Name
                    Target = $identity.Target
                }
            }
            else
            {
                [PSCustomObject]@{
                    Source = ('{0}\{1}' -f $sourceDomainNetBios, $identity.Name)
                    Target = ('{0}\{1}' -f $destDomainNetBios, $identity.Target)
                }
                [PSCustomObject]@{
                    Source = ('{0}@{1}' -f $identity.Name, $sourceDomainDNS)
                    Target = ('{0}@{1}' -f $identity.Target, $destDomainDNS)
                }
            }
        }
        #endregion Preparing imported identities
    }
    process
    {
        #region Preparing basic migration table
        $groupPolicyManager = New-Object -ComObject GPMgmt.GPM
        $migrationTable = $groupPolicyManager.CreateMigrationTable()
        $constants = $groupPolicyManager.getConstants()
        $backupDirectory = $groupPolicyManager.GetBackupDir($resolvedBackupPath)
        $backupList = $backupDirectory.SearchBackups($groupPolicyManager.CreateSearchCriteria())
        
        foreach ($policyBackup in $backupList)
        {
            $migrationTable.Add(0, $policyBackup)
            $migrationTable.Add($constants.ProcessSecurity, $policyBackup)
        }
        #endregion Preparing basic migration table
        
        #region Applying identity and UNC mappings
        foreach ($entry in $migrationTable.GetEntries())
        {
            switch ($entry.EntryType)
            {
                $constants.EntryTypeUNCPath
                {
                    if ($entry.Source -like "\\$sourceDomainDNS\*")
                    {
                        $null = $migrationTable.UpdateDestination($entry.Source, $entry.Source.Replace("\\$sourceDomainDNS\", "\\$destDomainDNS\"))
                    }
                    if ($entry.Source -like "\\$sourceDomainNetBios\*")
                    {
                        $null = $migrationTable.UpdateDestination($entry.Source, $entry.Source.Replace("\\$sourceDomainNetBios\", "\\$destDomainNetBios\"))
                    }
                }
                
                { $constants.EntryTypeUser, $constants.EntryTypeGlobalGroup, $constants.EntryTypeUniversalGroup, $constants.EntryTypeUnknown -contains $_ } {
                    if ($mapping = $explicitIdentityMappings | Where-Object Source -EQ $entry.Source)
                    {
                        $null = $migrationTable.UpdateDestination($entry.Source, $mapping.Target)
                    }
                }
            }
        }
        #endregion Applying identity and UNC mappings
        
        $migrationTable.Save($writePath)
        $writePath
    }
}

function Resolve-ADPrincipal
{
<#
    .SYNOPSIS
        Resolves an AD Principal into a common format.
     
    .DESCRIPTION
        Resolves an AD Principal into a common format.
        Optimized for use with cross-domain migration procedures.
     
        Caches successful results.
        Returns empty values on unresolved users.
     
    .PARAMETER Name
        Name of the principal to resolve.
     
    .PARAMETER Domain
        Domain to resolve it for.
        Read access is required.
     
    .EXAMPLE
        PS C:\> Resolve-ADPrincipal -Name 'contoso\max' -Domain 'contoso.com'
     
        Resolves the user max from contoso.com
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Domain
    )
    
    begin
    {
        if (-not $script:principals) { $script:principals = @{ } }
        if (-not $script:principals[$Domain]) { $script:principals[$Domain] = @{ } }
        
        $domainFQDN = (Get-ADDomain -Server $Domain).DNSRoot
        $domainName = (Get-ADDomain -Server $Domain).Name
    }
    process
    {
        foreach ($identity in $Name)
        {
            # Return form Cache if available
            if ($script:principals[$Domain][$identity])
            {
                return $script:principals[$Domain][$identity]
            }
            
            #region Resolve User in AD
            if ($identity -as [System.Security.Principal.SecurityIdentifier])
            {
                $adObject = Get-ADObject -Server $Domain -LDAPFilter "(objectSID=$identity)" -Properties ObjectSID
            }
            elseif (Test-IsDistinguishedName -Name $identity)
            {
                $adObject = Get-ADObject -Server ($identity | ConvertTo-DnsDomainName) -Identity $identity -Properties ObjectSID
            }
            elseif ($identity -like "*\*")
            {
                try { $sidName = ([System.Security.Principal.NTAccount]$identity).Translate([System.Security.Principal.SecurityIdentifier]) }
                catch { continue }
                $adObject = Get-ADObject -Server $Domain -LDAPFilter "(objectSID=$sidName)" -Properties ObjectSID
                if (-not $adObject)
                {
                    $script:principals[$Domain][$identity] = [pscustomobject]@{
                        DistinguishedName = $null
                        Name              = $identity
                        SID                  = $sidName.Value
                        RID                  = $sidName.Value.ToString().Split("-")[-1]
                        Type              = 'Local BuiltIn'
                        IsBuiltin          = $true
                        DomainName          = $domainName
                        DomainFqdn          = $domainFQDN
                    }
                    $script:principals[$Domain][$identity]
                    continue
                }
            }
            else
            {
                try
                {
                    $sidName = ([System.Security.Principal.NTAccount]$identity).Translate([System.Security.Principal.SecurityIdentifier])
                    if ($sidName.Value -like 'S-1-3-*')
                    {
                        $script:principals[$Domain][$identity] = [pscustomobject]@{
                            DistinguishedName = $null
                            Name              = $identity
                            SID                  = $sidName.Value
                            RID                  = $sidName.Value.ToString().Split("-")[-1]
                            Type              = 'Local BuiltIn'
                            IsBuiltin          = $true
                            DomainName          = $domainName
                            DomainFqdn          = $domainFQDN
                        }
                        $script:principals[$Domain][$identity]
                        continue
                    }
                    $adObject = Get-ADObject -Server $Domain -LDAPFilter "(objectSID=$sidName)" -Properties ObjectSID
                }
                catch
                {
                    $adObject = Get-ADObject -Server $Domain -LDAPFilter "(Name=$identity)" -Properties ObjectSID
                }
            }
            if (-not $adObject -or -not $adObject.ObjectSID) { continue }
            #endregion Resolve User in AD
            
            $script:principals[$Domain][$identity] = [pscustomobject]@{
                DistinguishedName = $adObject.DistinguishedName
                Name              = $adObject.Name
                SID                  = $adObject.ObjectSID.Value
                RID                  = $adObject.ObjectSID.Value.ToString().Split("-")[-1]
                Type              = $adObject.ObjectClass
                IsBuiltin          = ((($adObject.ObjectSID.Value.Split("-")[-1] -as [int]) -lt 1000) -or ($adObject.ObjectSID.Value -like 'S-1-5-32-*'))
                DomainName          = $domainName
                DomainFqdn          = $domainFQDN
            }
            $script:principals[$Domain][$identity]
        }
    }
}

function Test-IsDistinguishedName
{
<#
    .SYNOPSIS
        Lightweight test to check whether a string is a distinguished name.
     
    .DESCRIPTION
        Lightweight test to check whether a string is a distinguished name.
        This check is done by checking, whether the string contains a "DC=" sequence.
     
    .PARAMETER Name
        The name to check.
     
    .EXAMPLE
        PS C:\> Test-IsDistinguishedName -Name $name
     
        returns whether $name is a distinguished name.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )
    
    process
    {
        $Name -match 'DC='
    }
}

function Test-Overlap
{
<#
    .SYNOPSIS
        Matches N:N mappings for congruence.
     
    .DESCRIPTION
        Matches N:N mappings for congruence.
        Use this for comparing two arrays for overlap.
        This can be used for scenarios such as:
        - Whether n Items in Array One are equal to an Item in Array Two.
        - Whether n Items in Array One are similar to an Item in Array Two.
        This is especially designed to abstract filtering by multiple wildcard filters.
     
    .PARAMETER ReferenceObject
        The object(s) to compare
     
    .PARAMETER DifferenceObject
        The array of items to compare them to.
     
    .PARAMETER Property
        Compare a property, rather than the basic object.
     
    .PARAMETER Count
        The number of congruent items required for a successful result.
        Defaults to 1.
     
    .PARAMETER Operator
        How the comparison should be performed.
        Defaults to 'Equal'
        Supported Comparisons: Equal, Like, Match
     
    .EXAMPLE
        PS C:\> Test-Overlap -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject
     
        Tests whether any item in the two arrays are equal.
#>

    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $ReferenceObject,
        
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $DifferenceObject,
        
        [string]
        $Property,
        
        [int]
        $Count = 1,
        
        [ValidateSet('Equal', 'Like', 'Match')]
        [string]
        $Operator = 'Equal'
    )
    
    begin
    {
        $parameter = @{
            IncludeEqual = $true
            ExcludeDifferent = $true
        }
        if ($Property) { $parameter['Property'] = $Property }
    }
    process
    {
        switch ($Operator)
        {
            'Equal'
            {
                return (Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DebugPreference @parameter | Measure-Object).Count -ge $Count
            }
            'Like'
            {
                $numberFound = 0
                foreach ($reference in $ReferenceObject)
                {
                    foreach ($difference in $DifferenceObject)
                    {
                        if ($Property -and ($reference.$Property -like $difference.$Property)) { $numberFound++ }
                        elseif (-not $Property -and ($reference -like $difference)) { $numberFound++ }
                        
                        if ($numberFound -ge $Count) { return $true }
                    }
                }
                
                return $false
            }
            'Match'
            {
                $numberFound = 0
                foreach ($reference in $ReferenceObject)
                {
                    foreach ($difference in $DifferenceObject)
                    {
                        if ($Property -and ($reference.$Property -match $difference.$Property)) { $numberFound++ }
                        elseif (-not $Property -and ($reference -match $difference)) { $numberFound++ }
                        
                        if ($numberFound -ge $Count) { return $true }
                    }
                }
                
                return $false
            }
        }
    }
}


function Backup-GptPolicy
{
<#
    .SYNOPSIS
        Creates a full backup of all specified GPOs.
     
    .DESCRIPTION
        Creates a full backup of all specified GPOs.
        This includes permissions, settings, GPO Links and WMI Filter.
     
    .PARAMETER Path
        The path to the folder to export into.
        Folder must exist.
     
    .PARAMETER Name
        Filter Policy Objects by policy name.
        By default, ALL policies are targeted.
     
    .PARAMETER GpoObject
        Specify explicitly which GPOs to export.
        Accepts output of Get-GPO
     
    .PARAMETER Domain
        The source domain to export from.
     
    .PARAMETER Identity
        Additional identities to export.
        Identites are names of groups that are used for matching groups when importing policies.
     
    .EXAMPLE
        PS C:\> Backup-GptPolicy -Path .
     
        Export all policies to file.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string]
        $Name = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $GpoObject,
        
        [string]
        $Domain = $env:USERDNSDOMAIN,
        
        [string[]]
        $Identity
    )
    
    begin
    {
        $resolvedPath = (Resolve-Path -Path $Path).ProviderPath
        $policyFolder = New-Item -Path $resolvedPath -Name GPO -ItemType Directory -Force
    }
    process
    {
        $gpoObjects = $GpoObject
        if (-not $GpoObject)
        {
            $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name
        }
        $gpoObjects | Export-GptObject -Path $policyFolder.FullName -Domain $Domain
        Export-GptLink -Path $resolvedPath -Domain $Domain
        $gpoObjects | Export-GptPermission -Path $resolvedPath -Domain $Domain
        $gpoObjects | Export-GptWmiFilter -Path $resolvedPath -Domain $Domain
        Export-GptIdentity -Path $resolvedPath -Domain $Domain -Name $Identity
        Export-GptDomainData -Path $resolvedPath -Domain $Domain
    }
}


function Export-GptDomainData
{
<#
    .SYNOPSIS
        Generates a summary export of the source domain.
     
    .DESCRIPTION
        Generates a summary export of the source domain.
        This data is required or useful in several import stages.
     
    .PARAMETER Path
        The path to export to.
        Point at an existing folder.
     
    .PARAMETER Domain
        The domain to export the info of.
     
    .EXAMPLE
        PS C:\> Export-GptDomainData -Path '.'
     
        Exports the current domain's basic info into the current folder.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $resolvedPath = (Resolve-Path -Path $Path).ProviderPath
    }
    process
    {
        $domainObject = Get-ADDomain -Server $Domain
        [pscustomobject]@{
            Domain          = $Domain
            DomainDNSName = $domainObject.DNSRoot
            NetBIOSName   = $domainObject.NetBIOSName
            BackupVersion = '1.0.0'
            Timestamp      = (Get-Date)
            DomainSID      = $domainObject.DomainSID.Value
        } | Export-Clixml -Path (Join-Path -Path $resolvedPath -ChildPath 'backup.clixml')
    }
}

function Export-GptIdentity
{
<#
    .SYNOPSIS
        Exports identity data used for Group Policy imports.
     
    .DESCRIPTION
        Generates an export dump of identity information.
        This is later used during import of group policy objects:
        - To map between identities for permissions and policy content.
        - To translate localized builtin account names.
        - To correctly target renamed builtin acconts.
     
    .PARAMETER Path
        The path where the exprot should be stored in.
        Specify an existing folder.
     
    .PARAMETER Name
        Names of groups to include in addition to the builtin accounts.
     
    .PARAMETER Domain
        The domain to generate the dump from.
     
    .EXAMPLE
        PS C:\> Export-GptIdentity -Path '.'
     
        Export the builtin accounts into the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string[]]
        $Name,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator
        
        [System.Collections.ArrayList]$identities = @()
    }
    process
    {
        #region Process Builtin Accounts
        $builtInSID = 'S-1-5-32-544', 'S-1-5-32-545', 'S-1-5-32-546', 'S-1-5-32-548', 'S-1-5-32-549', 'S-1-5-32-550', 'S-1-5-32-551', 'S-1-5-32-552', 'S-1-5-32-554', 'S-1-5-32-555', 'S-1-5-32-556', 'S-1-5-32-557', 'S-1-5-32-558', 'S-1-5-32-559', 'S-1-5-32-560', 'S-1-5-32-561', 'S-1-5-32-562', 'S-1-5-32-568', 'S-1-5-32-569', 'S-1-5-32-573', 'S-1-5-32-574', 'S-1-5-32-575', 'S-1-5-32-576', 'S-1-5-32-577', 'S-1-5-32-578', 'S-1-5-32-579', 'S-1-5-32-580', 'S-1-5-32-582'
        $builtInRID = '498', '500', '501', '502', '512', '513', '514', '515', '516', '517', '518', '519', '520', '521', '522', '525', '526', '527', '553', '571', '572'
        $domainSID = (Get-ADDomain -Server $pdcEmulator).DomainSID.Value
        $identities.AddRange(($builtInSID | Resolve-ADPrincipal -Domain $Domain))
        $identities.AddRange(($builtInRID | Resolve-ADPrincipal -Domain $Domain -Name { '{0}-{1}' -f $domainSID, $_ }))
        #endregion Process Builtin Accounts
        
        #region Process Additional Requested Accounts
        foreach ($adEntity in $Name)
        {
            #region Handle Wildcard Filters
            if ($adEntity.Contains("*"))
            {
                $identities.AddRange((Get-ADGroup -Server $pdcEmulator -LDAPFilter "(name=$adEntity)" | Resolve-ADPrincipal -Domain $Domain))
                continue
            }
            #endregion Handle Wildcard Filters
            try
            {
                $principal = Resolve-ADPrincipal -Name $adEntity -Domain $Domain -ErrorAction Stop
                $null = $identities.Add($principal)
            }
            catch { Write-Error -Message "Failed to resolve Identity: $adEntity | $_" -Exception $_.Exception }
        }
        #endregion Process Additional Requested Accounts
    }
    end
    {
        $identities | Group-Object SID | ForEach-Object {
            $_.Group | Select-Object -First 1
        } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_Identities_$($Domain).csv") -Encoding UTF8 -NoTypeInformation
    }
}

function Export-GptLink
{
<#
    .SYNOPSIS
        Generates a full dump of all GPO links.
     
    .DESCRIPTION
        Generates a full dump of all GPO links.
        This command will enumerate all OUs and create an export file of them.
        This is used to restore links of exported GPOs when restoring them.
     
    .PARAMETER Path
        The path in which to export the data.
        Specify an existing folder.
     
    .PARAMETER Domain
        The domain to retrieve the data from.
     
    .EXAMPLE
        PS C:\> Export-GptLink -Path .
     
        Exports all GPO links into the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $gpoObjects = Get-GPO -All -Domain $Domain
    }
    process
    {
        Get-ADOrganizationalUnit -Server $Domain -LdapFilter '(gpLink=*)' -Properties gpLink, CanonicalName | ForEach-Object {
            $indexCount = 0
            $links = $_.gpLink -replace '\]\[', ']_[' -split '_'
            foreach ($link in $links)
            {
                $path, $state = $link -replace '\[LDAP://' -replace '\]$' -split ';'
                [PSCustomObject]@{
                    Path    = $Path
                    State   = $state # 0: Normal, 1: Disabled, 2: Enforced
                    GpoName = ($gpoObjects | Where-Object Path -EQ $path).DisplayName
                    Domain  = $Domain
                    OUDN    = $_.DistinguishedName
                    OUName  = $_.Name
                    OUCanonical = $_.CanonicalName
                    Index   = $indexCount++
                    TotalCount = $links.Count
                }
            }
        } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_Links_$($Domain).csv") -Encoding UTF8 -NoTypeInformation
    }
}


function Export-GptObject
{
<#
    .SYNOPSIS
        Creates a backup of all specified GPOs.
     
    .DESCRIPTION
        Creates a backup of all specified GPOs.
     
    .PARAMETER Path
        The path in which to generate the Backup.
     
    .PARAMETER Name
        The name to filter GPOs by.
        By default, ALL GPOs are exported.
     
    .PARAMETER GpoObject
        Select the GPOs to export by specifying the explicit GPO object to export.
     
    .PARAMETER Domain
        The domain from which to export the GPOs
     
    .EXAMPLE
        PS C:\> Export-GptObject -Path .
     
        Generate a GPO export of all GPOs in the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string[]]
        $Name = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $GpoObject,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    process
    {
        $gpoObjects = $GpoObject | Where-Object {
            Test-Overlap -ReferenceObject $_.DisplayName -DifferenceObject $Name -Operator Like
        }
        if (-not $GpoObject)
        {
            $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object {
                Test-Overlap -ReferenceObject $_.DisplayName -DifferenceObject $Name -Operator Like
            }
        }
        $null = $gpoObjects | Backup-GPO -Path (Resolve-Path $Path).ProviderPath
        $gpoObjects | Select-Object DisplayName, ID, Owner, CreationTime, ModificationTime, WmiFilter | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_object_$($Domain).csv") -Encoding UTF8 -NoTypeInformation -Append
    }
}


function Export-GptPermission
{
<#
    .SYNOPSIS
        Export the permissions assigned on GPOs
     
    .DESCRIPTION
        Export the permissions assigned on GPOs.
         
        Note: This command is currently fairly slow so give it some time.
     
    .PARAMETER Path
        The path where to create the export.
        Must be an existing folder.
     
    .PARAMETER Name
        Filter GPOs to process by name.
     
    .PARAMETER GpoObject
        Specify GPOs to process by object.
     
    .PARAMETER IncludeInherited
        Include inherited permissions in the export.
        By default, only explicit permissiosn are exported.
        Note: By default, all GPOs in a windows domain only have explicit permissions set.
        This will have little impact in most scenarios.
     
    .PARAMETER Domain
        The domain to export from.
     
    .EXAMPLE
        PS C:\> Export-GptPermission -Path '.'
     
        Exports permissions of all GPOs into the current folder.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ })]
        [string]
        $Path,
        
        [string]
        $Name = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $GpoObject,
        
        [switch]
        $IncludeInherited,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        Write-Verbose "Preparing Filters"
        $select_Name = @{ name = 'GpoName'; expression = { $gpoItem.DisplayName } }
        $select_Path = @{ name = 'GpoPath'; expression = { $gpoItem.Path } }
        $select_SID = @{ name = 'SID'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).SID } }
        $select_RID = @{ name = 'RID'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).RID } }
        $select_IsBuiltin = @{ name = 'IsBuiltIn'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).IsBuiltIn } }
        $select_PrincipalType = @{ name = 'PrincipalType'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).Type } }
        
        [System.Collections.ArrayList]$accessList = @()
    }
    process
    {
        Write-Verbose "Resolving Policies to process"
        $gpoObjects = $GpoObject
        if (-not $GpoObject)
        {
            $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name
        }
        Write-Verbose "Found $($gpoObjects.Count) Policies"
        $accessData = foreach ($gpoItem in $gpoObjects)
        {
            Write-Verbose "Processing policy: $($gpoItem.DisplayName)"
            $adObject = Get-ADObject -Identity $gpoItem.Path -Server $gpoItem.DomainName -Properties ntSecurityDescriptor
            $adObject.ntSecurityDescriptor.Access | Where-Object {
                $IncludeInherited -or -not $_.IsInherited
            } | Select-Object $select_Name, $select_Path, '*', $select_SID, $select_RID, $select_IsBuiltin, $select_PrincipalType
        }
        Write-Verbose "Found $($accessData.Count) permission entries."
        $null = $accessList.AddRange($accessData)
    }
    end
    {
        Write-Verbose "Exorting to file"
        $accessList | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_permissions_$($Domain).csv") -Encoding UTF8 -NoTypeInformation
    }
}


function Export-GptWmiFilter
{
<#
    .SYNOPSIS
        Export WMI Filters.
     
    .DESCRIPTION
        Export WMI Filters.
        WMI Filters to export are picked up by the GPÜO they are assigned to.
        Unassigned filters are ignored.
     
    .PARAMETER Path
        The path where to create the export.
        Must be an existing folder.
     
    .PARAMETER Name
        Filter GPOs to process by name.
     
    .PARAMETER GpoObject
        Specify GPOs to process by object.
     
    .PARAMETER Domain
        The domain to export from.
     
    .EXAMPLE
        PS C:\> Export-GptWmiFilter -Path '.'
     
        Export all WMI Filters of all GPOs into the current folder.
#>

    [CmdletBinding()]
    param (
        [ValidateScript({ Test-Path -Path $_ })]
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string]
        $Name = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $GpoObject,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $wmiPath = "CN=SOM,CN=WMIPolicy,$((Get-ADDomain -Server $Domain).SystemsContainer)"
        $allFilterHash = @{ }
        $foundFilterHash = @{ }
        
        Get-ADObject -Server $Domain -SearchBase $wmiPath -Filter { objectClass -eq 'msWMI-Som' } -Properties msWMI-Author, msWMI-Name, msWMI-Parm1, msWMI-Parm2 | ForEach-Object {
            $allFilterHash[$_.'msWMI-Name'] = [pscustomobject]@{
                Author = $_.'msWMI-Author'
                Name   = $_.'msWMI-Name'
                Description = $_.'msWMI-Parm1'
                Filter = $_.'msWMI-Parm2'
            }
        }
    }
    process
    {
        $gpoObjects = $GpoObject
        if (-not $GpoObject)
        {
            $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name
        }
        foreach ($filterName in $gpoObjects.WmiFilter.Name)
        {
            $foundFilterHash[$filterName] = $allFilterHash[$filterName]
        }
    }
    end
    {
        $foundFilterHash.Values | Where-Object { $_ } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_wmifilters_$($Domain).csv") -Encoding UTF8 -NoTypeInformation
    }
}


function Import-GptDomainData
{
<#
    .SYNOPSIS
        Imports domain information of the source domain.
     
    .DESCRIPTION
        Imports domain information of the source domain.
     
    .PARAMETER Path
        The path to the file or the folder it resides in.
     
    .EXAMPLE
        PS C:\> Import-GptDomainData -Path '.'
     
        Import the domain information file from the current folder.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )
    
    begin
    {
        $pathItem = Get-Item -Path $Path
        if ($pathItem.Extension -eq '.clixml') { $resolvedPath = $pathItem.FullName }
        else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'backup.clixml' | Select-Object -First 1).FullName }
        if (-not $resolvedPath) { throw "Could not find a domain data file in $($pathItem.FullName)" }
    }
    process
    {
        $script:sourceDomainData = Import-Clixml $resolvedPath
    }
}

function Import-GptIdentity
{
<#
    .SYNOPSIS
        Imports identity data exported from the source domain.
     
    .DESCRIPTION
        Imports identity data exported from the source domain.
        This data is used for mapping source identities to destination identities.
     
    .PARAMETER Path
        The path where to pick up the file.
     
    .PARAMETER Name
        Filter identities by name.
     
    .PARAMETER Domain
        The destination domain that later GPOs will be imported to.
     
    .PARAMETER Mapping
        A mapping hashtable allowing you to map identities that have unequal names.
     
    .EXAMPLE
        PS C:\> Import-GptIdentity -Path '.'
     
        Import the identity export file from the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ })]
        [string]
        $Path,
        
        [string[]]
        $Name = '*',
        
        [string]
        $Domain = $env:USERDNSDOMAIN,
        
        [System.Collections.IDictionary]
        $Mapping = @{ }
    )
    
    begin
    {
        $pathItem = Get-Item -Path $Path
        if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName }
        else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_Identities*.csv' | Select-Object -First 1).FullName }
        if (-not $resolvedPath) { throw "Could not find identities file in $($pathItem.FullName)" }
        
        $domainSID = (Get-ADDomain -Server $Domain).DomainSID.Value
        
        # Declare Module scope index of identities and what they map to
        $script:identityMapping = New-Object 'System.Collections.Generic.List[Object]'
        
        # Helpful Select Hashtables
        $select_TargetMapping = @{
            Name       = 'Target'
            Expression = { $Mapping[$importEntry.Name] }
        }
        $select_TargetName = @{
            Name       = 'Target'
            Expression = { $targetName }
        }
    }
    process
    {
        $importData = Import-Csv -Path $resolvedPath
        foreach ($importEntry in $importData)
        {
            # Skip entries filtered out
            if (-not (Test-Overlap -ReferenceObject $importEntry.Name -DifferenceObject $Name -Operator Like))
            {
                continue
            }
            
            #region Case: Mapped Entry
            if ($Mapping[$importEntry.Name])
            {
                $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetMapping))
            }
            #endregion Case: Mapped Entry
            
            #region Case: Discovery
            else
            {
                #region Case: Native BuiltIn Principal
                if (($importEntry.IsBuiltIn -eq 'True') -and ($importEntry.SID -like "*-32-*"))
                {
                    try { $targetName = ([System.Security.Principal.SecurityIdentifier]$importEntry.SID).Translate([System.Security.Principal.NTAccount]).Value }
                    catch
                    {
                        Write-Warning "Failed to translate identity: $($importEntry.Name) ($($importEntry.SID))"
                        continue
                    }
                    $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName))
                }
                #endregion Case: Native BuiltIn Principal
                
                #region Case: Domain Specific BuiltIn Principal
                elseif ($importEntry.IsBuiltIn -eq 'True')
                {
                    $targetSID = '{0}-{1}' -f $domainSID, $importEntry.RID
                    $adObject = Get-ADObject -Server $Domain -LDAPFilter "(&(objectClass=$($importEntry.Type))(objectSID=$($targetSID)))"
                    if (-not $adObject)
                    {
                        Write-Warning "Failed to resolve AD identity: $($importEntry.Name) ($($targetSID))"
                        continue
                    }
                    $targetName = $adObject.Name
                    $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName))
                }
                #endregion Case: Domain Specific BuiltIn Principal
                
                #region Case: Custom Principal
                else
                {
                    $adObject = Get-ADObject -Server $Domain -LDAPFilter "(&(objectClass=$($importEntry.Type))(name=$($importEntry.Name)))"
                    if (-not $adObject)
                    {
                        Write-Warning "Failed to resolve AD identity: $($importEntry.Name)"
                        continue
                    }
                    $targetName = $adObject.Name
                    $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName))
                }
                #endregion Case: Custom Principal
            }
            #endregion Case: Discovery
        }
    }
}

function Import-GptLink
{
<#
    .SYNOPSIS
        Imports GPO Links.
     
    .DESCRIPTION
        Imports GPO Links.
        Use this to restore the exported links in their original order (or as close to it as possible).
     
    .PARAMETER Path
        The path from which to pick up the import file.
     
    .PARAMETER Name
        Only restore links of matching GPOs
     
    .PARAMETER Domain
        The domain into which to import.
     
    .EXAMPLE
        PS C:\> Import-GptLink -Path '.'
     
        Import GPO Links based on the exported links stored in the current path.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string[]]
        $Name = '*',
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        #region Utility Functions
        function Get-OU
        {
        <#
            .SYNOPSIS
                Retrieves an OU. Caches results.
             
            .DESCRIPTION
                Retrieves an OU. Caches results.
                Results are cached separately for each domain/server.
             
            .PARAMETER DistinguishedName
                The name of the OU to check.
             
            .PARAMETER Server
                The domain or server to check against.
             
            .EXAMPLE
                PS C:\> Get-OU -DistinguishedName $dn -Server $Domain
             
                Return the OU pointed at with $dn if it exists.
        #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [string]
                $DistinguishedName,
                
                [Parameter(Mandatory = $true)]
                [string]
                $Server
            )
            
            if (-not $script:targetOUs) { $script:targetOUs = @{ } }
            if (-not $script:targetOUs[$Server]) { $script:targetOUs[$Server] = @{ } }
            
            if ($script:targetOUs[$Server].ContainsKey($DistinguishedName))
            {
                return $script:targetOUs[$Server][$DistinguishedName]
            }
            
            try
            {
                $paramGetADOrganizationalUnit = @{
                    Identity    = $DistinguishedName
                    Server        = $Server
                    Properties  = 'gpLink'
                    ErrorAction = 'Stop'
                }
                $script:targetOUs[$Server][$DistinguishedName] = Get-ADOrganizationalUnit @paramGetADOrganizationalUnit
            }
            catch { $script:targetOUs[$Server][$DistinguishedName] = $null }
            return $script:targetOUs[$Server][$DistinguishedName]
        }
        
        function Set-GPLinkSet
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                $LinkObject,
                
                $Domain,
                
                $AllGpos,
                
                $Server
            )
            
            foreach ($linkItem in $LinkObject)
            {
                $linkItem.Index = [int]($linkItem.Index)
                $linkItem.TotalCount = [int]($linkItem.TotalCount)
            }
            $orgUnit = Get-OU -DistinguishedName $LinkObject[0].TargetOU -Server $Domain
            $insertIndex = 1
            foreach ($linkItem in ($LinkObject | Sort-Object Index))
            {
                if ($orgUnit.LinkedGroupPolicyObjects -contains $linkItem.Policy.CleanedPath)
                {
                    $insertIndex = $orgUnit.LinkedGroupPolicyObjects.IndexOf($linkItem.Policy.CleanedPath) + 1
                    continue
                }
                
                $paramSetGPLink = @{
                    LinkEnabled = 'Yes'
                    Guid        = $linkItem.Policy.ID
                    Order        = $insertIndex
                    Domain        = $Domain
                    Enforced    = 'No'
                    Target        = $orgUnit
                    Server        = $Server
                    ErrorAction = 'Stop'
                }
                if ($linkItem.State -eq "1") { $paramSetGPLink['LinkEnabled'] = 'No' }
                if ($linkItem.State -eq "2") { $paramSetGPLink['Enforced'] = 'Yes' }
                
                try
                {
                    $null = New-GPLink @paramSetGPLink
                    New-ImportResult -Action 'Importing Group Policy Links' -Step 'Applying Link' -Target $linkItem.GpoName -Data $linkItem -Success $true
                }
                catch
                {
                    New-ImportResult -Action 'Importing Group Policy Links' -Step 'Applying Link' -Target $linkItem.GpoName -Data $linkItem -Success $false -ErrorData $_
                }
                
                $insertIndex++
            }
        }
        #endregion Utility Functions
        
        $PSDefaultParameterValues['New-ImportResult:Action'] = 'Importing Group Policy Links'
        $PSDefaultParameterValues['New-ImportResult:Success'] = $false
        
        $pathItem = Get-Item -Path $Path
        if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName }
        else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_links_*.csv' | Select-Object -First 1).FullName }
        if (-not $resolvedPath) { throw "Could not find GPO Links file in $($pathItem.FullName)" }
        
        $domainObject = Get-ADDomain -Server $Domain
        $policyObjects = Get-GPO -All -Domain $Domain | Select-Object *, @{
            Name       = 'CleanedPath'
            Expression = { $_.Path -replace $_.ID, $_.ID }
        }
        $linkData = Import-Csv $resolvedPath | Where-Object {
            Test-Overlap -ReferenceObject $_.GpoName -DifferenceObject $Name -Operator Like
        } | Select-Object *, @{
            Name            = "Policy"
            Expression        = {
                $linkItem = $_
                $policyObjects | Where-Object DisplayName -EQ $linkItem.GpoName
            }
        }, @{
            Name                                                                       = "TargetOU"
            Expression                                                                   = {
                '{0},{1}' -f ($_.OUDN -replace ',DC=\w+'), $domainObject.DistinguishedName
            }
        }
    }
    process
    {
        $groupedLinks = $linkData | Group-Object -Property GpoName
        $groupedLinks | Where-Object Name -NotIn $policyObjects.DisplayName | ForEach-Object {
            New-ImportResult -Step 'Checking GPO existence' -Target $_.Name -Data $_.Group -ErrorData "GPO $($_.Name) does not exist"
        }
        $linksPolicyExists = ($groupedLinks | Where-Object Name -In $policyObjects.DisplayName).Group
        $linksPolicyExists | Where-Object { -not (Get-OU -DistinguishedName $_.TargetOU -Server $Domain) } | ForEach-Object {
            New-ImportResult -Step 'Checking OU existence' -Target $_.GpoName -Data $_ -ErrorData "OU $($_.TargetOU) does not exist, cannot link $($_.GpoName)"
        }
        $linksToProcess = $linksPolicyExists | Where-Object { Get-OU -DistinguishedName $_.TargetOU -Server $Domain }
        
        $groupedToProcess = $linksToProcess | Group-Object -Property TargetOU
        foreach ($linkSet in $groupedToProcess)
        {
            Set-GPLinkSet -LinkObject $linkSet.Group -Domain $domainObject.DNSRoot -AllGpos $policyObjects -Server $domainObject.PDCEmulator
        }
    }
}


function Import-GptObject
{
<#
    .SYNOPSIS
        Import Group Policy Objects previously exported using Export-GptObject.
     
    .DESCRIPTION
        Import Group Policy Objects previously exported using Export-GptObject.
     
    .PARAMETER Path
        The path where the GPO export folders are located.
        Note: GPO export folders have a GUID as name.
     
    .PARAMETER Name
        Only import GPOs with a matching name.
     
    .PARAMETER Domain
        THe destination domain to import into.
     
    .EXAMPLE
        PS C:\> Import-GptObject -Path '.'
     
        Import all GPO objects exported into the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string[]]
        $Name = '*',
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator
        if (-not (Test-Path $Path))
        {
            New-ImportResult -Action 'Importing Policy Objects' -Step 'Validating import path' -Target $Path -Success $false
            throw "Import path not found: $Path"
        }
        if ((Get-Item -Path $Path).Extension -eq '.csv') { $gpoFile = Get-Item -Path $Path }
        elseif (Test-Path -Path (Join-Path -Path $Path -ChildPath 'gp_object_*.csv')) { $gpoFile = Get-Item (Join-Path -Path $Path -ChildPath 'gp_object_*.csv') }
        elseif (Test-Path -Path (Join-Path -Path (Join-Path -Path $Path -ChildPath 'GPO') -ChildPath 'gp_object_*.csv')) { $gpoFile = Get-Item (Join-Path -Path (Join-Path -Path $Path -ChildPath 'GPO') -ChildPath 'gp_object_*.csv') }
        else
        {
            New-ImportResult -Action 'Importing Policy Objects' -Step 'Validating import path' -Target $Path -Success $false
            throw "Could not find GPO backup index under: $Path"
        }
        $gpoData = Import-Csv -Path $gpoFile.FullName
        
        try { $migrationTablePath = New-MigrationTable -Path $gpoFile.DirectoryName -BackupPath $gpoFile.DirectoryName -Domain $Domain -ErrorAction Stop }
        catch
        {
            New-ImportResult -Action 'Importing Policy Objects' -Step 'Creating Migration Table' -Target $Path -Success $false -ErrorData $_
            throw
        }
    }
    process
    {
        foreach ($gpoEntry in $gpoData)
        {
            if (-not (Test-Overlap -ReferenceObject $gpoEntry.DisplayName -DifferenceObject $Name -Operator Like))
            {
                continue
            }
            
            $paramImportGPO = @{
                Domain          = $Domain
                Server          = $pdcEmulator
                BackupGpoName = $gpoEntry.DisplayName
                TargetName    = $gpoEntry.DisplayName
                Path          = $gpoFile.DirectoryName
                MigrationTable = $migrationTablePath
                CreateIfNeeded = $true
                ErrorAction   = 'Stop'
            }
            try
            {
                Write-Verbose "Importing Policy object: $($gpoEntry.DisplayName)"
                $importedGPO = Import-GPO @paramImportGPO
                if ($gpoEntry.WmiFilter)
                {
                    $wmiFilter = Get-ADObject -SearchBase "CN=SOM,CN=WMIPolicy,$((Get-ADDomain -Server $pdcEmulator).SystemsContainer)" -LDAPFilter "(&(objectClass=msWMI-Som)(msWMI-Name=$($gpoEntry.WmiFilter)))"
                    Set-ADObject -Identity $importedGPO.Path -Replace @{ gPCWQLFilter = "[$Domain;$($wmiFilter.Name);0]" } -Server $pdcEmulator
                }
                New-ImportResult -Action 'Importing Policy Objects' -Step 'Import Object' -Target $gpoEntry -Success $true -Data $gpoEntry, $migrationTablePath
            }
            catch
            {
                New-ImportResult -Action 'Importing Policy Objects' -Step 'Import Object' -Target $gpoEntry -Success $false -Data $gpoEntry, $migrationTablePath -ErrorData $_
                Write-Error $_
            }
        }
    }
}


function Import-GptPermission
{
<#
    .SYNOPSIS
        Import permissions to GPOs.
     
    .DESCRIPTION
        Import permissions to GPOs.
        This tries to restore the same permissions that existed on the GPOs before the export.
        Notes:
        - It is highly recommended to perform this before executing Import-GptLink.
        - Executing this requires the identities to have been imported (Import-GptIdentity)
     
    .PARAMETER Path
        The path where the permission export file is stored.
     
    .PARAMETER Name
        Only restore permissions for GPOs with a matching name.
     
    .PARAMETER GpoObject
        Select the GPOs to restore permissions to by specifying their full object.
     
    .PARAMETER ExcludeInherited
        Do not import permissions that were inherited permissions on the source GPO
     
    .PARAMETER Domain
        The domain to restore the GPO permissions to.
     
    .EXAMPLE
        PS C:\> Import-GptPermission -Path '.'
     
        Import GPO permissions from the current path.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ })]
        [string]
        $Path,
        
        [string[]]
        $Name = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $GpoObject,
        
        [switch]
        $ExcludeInherited,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        #region Utility Functions
        function Update-GpoPermission
        {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                $ADObject,
                
                $Permission,
                
                $GpoObject,
                
                $DomainObject
            )
            
            try
            {
                $accessRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList @(
                    (ConvertFrom-ImportedIdentity -Permission $Permission -DomainObject $DomainObject),
                    $Permission.ActiveDirectoryRights,
                    $Permission.AccessControlType,
                    $Permission.ObjectType,
                    $Permission.InheritanceType,
                    $Permission.InheritedObjectType
                )
            }
            catch
            {
                New-ImportResult -Action 'Update Gpo Permission' -Step 'Resolving Identity' -Target $Permission.GpoName -Success $false -Data $Permission -ErrorData $_
                return
            }
            
            $matchingRule = $null
            $matchingRule = $ADObject.ntSecurityDescriptor.Access | Where-Object {
                $accessRule.IdentityReference -eq $_.IdentityReference -and
                $accessRule.ActiveDirectoryRights -eq $_.ActiveDirectoryRights -and
                $accessRule.AccessControlType -eq $_.AccessControlType -and
                $accessRule.ObjectType -eq $_.ObjectType -and
                $accessRule.InheritanceType -eq $_.InheritanceType -and
                $accessRule.InheritedObjectType -eq $_.InheritedObjectType
            }
            
            if ($matchingRule)
            {
                New-ImportResult -Action 'Update Gpo Permission' -Step 'Skipped, already exists' -Target $Permission.GpoName -Success $true -Data $Permission, $accessRule
                return
            }
            
            #region Set AD Permissions
            try
            {
                Write-Verbose "Updating ACL on GPO $($ADObject.DistinguishedName)"
                $acl = Get-Acl -Path "AD:\$($ADObject.DistinguishedName)" -ErrorAction Stop
                $acl.AddAccessRule($accessRule)
                $acl | Set-Acl -Path "AD:\$($ADObject.DistinguishedName)" -ErrorAction Stop
            }
            catch
            {
                New-ImportResult -Action 'Update Gpo Permission' -Step 'Apply AD Permission' -Target $Permission.GpoName -Success $false -Data $Permission, $accessRule -ErrorData $_
                continue
            }
            #endregion Set AD Permissions
            
            #region Set File Permissions
            if (-not (Test-Path $ADObject.gPCFileSysPath))
            {
                New-ImportResult -Action 'Update Gpo Permission' -Step 'Apply File Permission' -Target $Permission.GpoName -Success $false -Data $Permission, $accessRule -ErrorData "Path not found"
                continue
            }
            try
            {
                $rights = 'Read'
                if ($accessRule.ActiveDirectoryRights -eq 983295) { $rights = 'FullControl' }
                $fileRule = New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
                    $accessRule.IdentityReference
                    $rights
                    $accessRule.AccessControlType
                )
                
                
                $acl = Get-Acl -Path $ADObject.gPCFileSysPath -ErrorAction Stop
                $acl.AddAccessRule($fileRule)
                $acl | Set-Acl -Path $ADObject.gPCFileSysPath -ErrorAction Stop
            }
            catch
            {
                [pscustomobject]@{
                    Action = 'Update Gpo Permission'
                    Step   = 'Apply File Permission'
                    Target = $Permission.GpoName
                    Success = $false
                    Data   = $Permission
                    Data2  = $accessRule
                    Error  = $_
                }
            }
            #endregion Set File Permissions
            
            New-ImportResult -Action 'Update Gpo Permission' -Step Success -Target $Permission.GpoName -Success $true -Data $Permission, $accessRule
        }
        #endregion Utility Functions
        
        $pathItem = Get-Item -Path $Path
        if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName }
        else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_permissions_*.csv' | Select-Object -First 1).FullName }
        if (-not $resolvedPath) { throw "Could not find permissions file in $($pathItem.FullName)" }
        
        if (-not $script:identityMapping)
        {
            throw 'Could not find imported identities to match. Please run Import-GptIdentitiy first!'
        }
        
        $domainObject = Get-ADDomain -Server $Domain
        $allPermissionData = Import-Csv -Path $resolvedPath
    }
    process
    {
        $gpoObjects = $GpoObject
        if (-not $GpoObject)
        {
            $gpoObjects = Get-GPO -All -Domain $Domain
        }
        
        foreach ($gpoItem in $gpoObjects)
        {
            if (-not (Test-Overlap -ReferenceObject $gpoItem.DisplayName -DifferenceObject $Name -Operator Like))
            {
                continue
            }
            $adObject = Get-ADObject -Identity $gpoItem.Path -Server $gpoItem.DomainName -Properties ntSecurityDescriptor, gPCFileSysPath
            
            foreach ($permission in $allPermissionData)
            {
                # Skip items that do not apply
                if ($permission.GpoName -ne $gpoItem.DisplayName) { continue }
                if ($ExcludeInherited -and $permission.IsInherited -eq "True") { continue }
                
                Update-GpoPermission -ADObject $adObject -Permission $permission -GpoObject $gpoItem -DomainObject $domainObject
            }
        }
    }
}


function Import-GptWmiFilter
{
<#
    .SYNOPSIS
        Imports WMI filters.
     
    .DESCRIPTION
        Imports WMI filters stored to file using Export-GptWmiFilter.
        Note: This should be performed before using Import-GptPolicy.
         
    .PARAMETER Path
        The path from which to import the WmiFilters
     
    .PARAMETER Domain
        The domain into which to import the WmiFilters
     
    .EXAMPLE
        PS C:\> Import-GptWmiFilter -Path '.'
     
        Import WMI Filters from the current path.
#>

    [CmdletBinding()]
    param (
        [ValidateScript({ Test-Path -Path $_ })]
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $pathItem = Get-Item -Path $Path
        if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName }
        else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_wmifilters_*.csv' | Select-Object -First 1).FullName }
        if (-not $resolvedPath) { throw "Could not find WMI Filters file in $($pathItem.FullName)" }
        
        $allWmiFilterEntries = Import-Csv -Path $resolvedPath
        $namingContext = (Get-ADRootDSE -Server $Domain).DefaultNamingContext
        $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator
    }
    process
    {
        foreach ($wmiFilter in $allWmiFilterEntries)
        {
            #region Update Existing
            if ($adObject = Get-ADObject -Server $pdcEmulator -LDAPFilter "(&(objectClass=msWMI-Som)(msWMI-Name=$($wmiFilter.Name)))")
            {
                $adObject | Set-ADObject -Server $pdcEmulator -Replace @{
                    'msWMI-Author' = $wmiFilter.Author
                    'msWMI-Parm1'  = $wmiFilter.Description
                    'msWMI-Parm2'  = $wmiFilter.Filter
                }
            }
            #endregion Update Existing
            
            #region Create New
            else
            {
                $wmiGuid = "{$([System.Guid]::NewGuid())}"
                $creationDate = (Get-Date).ToUniversalTime().ToString("yyyyMMddhhmmss.ffffff-000")
                
                $attributes = @{
                    "showInAdvancedViewOnly" = "TRUE"
                    "msWMI-Name"             = $wmiFilter.Name
                    "msWMI-Parm1"             = $wmiFilter.Description
                    "msWMI-Parm2"             = $wmiFilter.Filter
                    "msWMI-Author"             = $wmiFilter.Author
                    "msWMI-ID"                 = $wmiGuid
                    "instanceType"             = 4
                    "distinguishedname"         = "CN=$wmiGuid,CN=SOM,CN=WMIPolicy,CN=System,$namingContext"
                    "msWMI-ChangeDate"         = $creationDate
                    "msWMI-CreationDate"     = $creationDate
                }
                
                $paramNewADObject = @{
                    OtherAttributes = $attributes
                    Name            = $wmiGuid
                    Type            = "msWMI-Som"
                    Path            = "CN=SOM,CN=WMIPolicy,CN=System,$namingContext"
                    Server            = $pdcEmulator
                }
                
                $null = New-ADObject @paramNewADObject
            }
            #endregion Create New
        }
    }
}


function Restore-GptPolicy
{
<#
    .SYNOPSIS
        Performs a full restore of GPOs exported with Backup-GptPolicy.
     
    .DESCRIPTION
        Performs a full restore of GPOs exported with Backup-GptPolicy.
        This includes executing all the relevant import commands in the optimal order.
     
    .PARAMETER Path
        The root path into which the backup was exported.
     
    .PARAMETER Name
        Only restore GPOs with matching name.
     
    .PARAMETER Domain
        The domain into which to restore the policy objects.
     
    .PARAMETER IdentityMapping
        A hashtable mapping source identities to destination identities.
        Use this to map groups that do not share the same name between source and destination.
     
    .EXAMPLE
        PS C:\> Restore-GptPolicy -Path '.'
     
        Perform a full restore/import of the backup written to the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string[]]
        $Name = '*',
        
        [string]
        $Domain = $env:USERDNSDOMAIN,
        
        [hashtable]
        $IdentityMapping = @{}
    )
    
    begin
    {
        $common = @{
            Path = $Path
            Domain = $Domain
        }
        
        Write-Verbose "Importing Domain Data"
        Import-GptDomainData -Path $Path
    }
    process
    {
        Write-Verbose "Importing Identities"
        Import-GptIdentity @common -Name $Name -Mapping $IdentityMapping
        Write-Verbose "Importing WMI Filters"
        Import-GptWmiFilter @common
        Write-Verbose "Importing Objects"
        Import-GptObject @common -Name $Name
        Write-Verbose "Importing Permissions"
        Import-GptPermission @common -Name $Name
        Write-Verbose "Importing GPO Links"
        Import-GptLink @common -Name $Name
    }
}


<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'GPOTools' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'GPOTools' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'GPOTools' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'GPOTools.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "GPOTools.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name GPOTools.alcohol
#>


New-PSFLicense -Product 'GPOTools' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-06-04") -Text @"
Copyright (c) 2019 Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code