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
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'
            {
                #TODO: Implement Domain Resolution
                try { $domainObject = Resolve-DomainMapping -DomainSid ($Permission.SID -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid.Value -DomainFqdn $Permission.DomainFqdn -DomainName $Permission.DomainName }
                catch { throw "Cannot resolve domain $($Permission.DomainFqdn) for $($Permission.Group) $($Permission.SID)! $_" }

                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 Get-DomainData
{
<#
    .SYNOPSIS
        Retrieves common domain data, while caching results.
     
    .DESCRIPTION
        Retrieves common domain data, while caching results.
        Reduces overhead of looking up the same object again and again.
     
    .PARAMETER Domain
        The domain to retrieve data for.
     
    .EXAMPLE
        PS C:\> Get-DomainData -Domain Contoso.com
     
        Returns domain data for the domain contoso.com
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Domain
    )
    
    begin
    {
        if (-not $script:domainData)
        {
            $script:domainData = @{ }
            
            #region Pre-Seed information for all domains in forest
            $forestObject = Get-ADForest
            $domains = $forestObject.Domains | Foreach-Object { Get-ADDomain -Server $_ -Identity $_ } | ForEach-Object {
                [PSCustomObject]@{
                    DistinguishedName = $_.DistinguishedName
                    Name              = $_.Name
                    SID                  = $_.DomainSID
                    Fqdn              = $_.DNSRoot
                    ADObject          = $_
                }
            }
            foreach ($domainObject in $domains)
            {
                $script:domainData["$($domainObject.SID)"] = $domainObject
                $script:domainData[$domainObject.Fqdn] = $domainObject
                $script:domainData[$domainObject.DistinguishedName] = $domainObject
            }
            #endregion Pre-Seed information for all domains in forest
        }
    }
    process
    {
        if ($script:domainData[$Domain])
        {
            return $script:domainData[$Domain]
        }
        
        #region Collect information for unknown domain
        if ($Domain -as [System.Security.Principal.SecurityIdentifier]) { $domainObject = Get-ADDomain -Identity $Domain -ErrorAction Stop }
        else { $domainObject = Get-ADDomain -Server $Domain -ErrorAction Stop }

        $domainObjectProcessed = [PSCustomObject]@{
            DistinguishedName = $domainObject.DistinguishedName
            Name              = $domainObject.Name
            SID                  = $domainObject.DomainSID
            Fqdn              = $domainObject.DNSRoot
            ADObject          = $domainObject
        }
        $script:domainData["$($domainObjectProcessed.SID)"] = $domainObjectProcessed
        $script:domainData[$domainObjectProcessed.Fqdn] = $domainObjectProcessed
        $script:domainData[$domainObjectProcessed.DistinguishedName] = $domainObjectProcessed
        $script:domainData[$Domain] = $domainObjectProcessed
        $script:domainData[$Domain]
        #endregion Collect information for unknown domain
    }
}

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
        $domainData = Get-DomainData -Domain $Domain
        $destDomainDNS = $domainData.Fqdn
        $destDomainNetBios = $domainData.ADObject.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 $identity.DomainName, $identity.Name)
                    Target = ('{0}\{1}' -f $identity.TargetDomain.Name, $identity.Target)
                }
                [PSCustomObject]@{
                    Source = ('{0}@{1}' -f $identity.Name, $identity.DomainFqdn)
                    Target = ('{0}@{1}' -f $identity.Target, $identity.TargetDomain.DNSRoot)
                }
            }
        }
        #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)
                    }
                }
            }
        }
        
        # Additionally scan backup for share mappings, as those won't be found by default
        foreach ($gpoFolder in (Get-ChildItem -Path $resolvedBackupPath -Directory | Where-Object Name -Match '^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$'))
        {
            $driveXmlPath = Join-Path -Path $gpoFolder.FullName -ChildPath 'DomainSysvol\GPO\User\Preferences\Drives\Drives.xml'
            if (-not (Test-Path -Path $driveXmlPath)) { continue }
            
            try { $driveXmlData = [xml](Get-Content -Path $driveXmlPath) }
            catch { continue }
            
            foreach ($driveSet in $driveXmlData.Drives.Drive)
            {
                if ($driveSet.Properties.Path -like "\\$sourceDomainDNS\*")
                {
                    $null = $migrationTable.AddEntry($driveSet.Properties.Path, $constants.EntryTypeUNCPath, $driveSet.Properties.Path.Replace("\\$sourceDomainDNS\", "\\$destDomainDNS\"))
                }
                if ($driveSet.Properties.Path -like "\\$sourceDomainNetBios\*")
                {
                    $null = $migrationTable.AddEntry($driveSet.Properties.Path, $constants.EntryTypeUNCPath, $driveSet.Properties.Path.Replace("\\$sourceDomainNetBios\", "\\$destDomainNetBios\"))
                }
            }
        }
        
        #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
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
    [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 = @{ } }
        
        $principalsToIgnore = @(
            # .NET Account sids, that are shared across all domains and need no translation
            'S-1-5-82-3876422241-1344743610-1729199087-774402673-2621913236'
            'S-1-5-82-271721585-897601226-2024613209-625570482-296978595'
            
            # Everyone, as it is 100% generic and has no domain-prefix
            'S-1-1-0'
            
            # NT Authority SIDs, as SID-to-SID need no translation, localization can be an issue
            'S-1-5-18'
            'S-1-5-19'
            'S-1-5-20'
        )
        
        $defaultDomainData = Get-DomainData -Domain $Domain
        $defaultDomainFQDN = $defaultDomainData.Fqdn
        $defaultDomainName = $defaultDomainData.Name
    }
    process
    {
        foreach ($identity in $Name)
        {
            if ($identity -in $principalsToIgnore) { continue }

            Write-Verbose "[Resolve-ADPrincipal] Resolving $identity"
            
            #region Resolve Principal Domain
            $domainFQDN = $defaultDomainFQDN
            $domainName = $defaultDomainName
            if ($identity -like "*@*")
            {
                $domainObject = Get-DomainData -Domain $identity.Split("@")[1]
                if ($domainObject)
                {
                    $domainFQDN = $domainObject.Fqdn
                    $domainName = $domainObject.Name
                }
            }
            elseif ($identity -as [System.Security.Principal.SecurityIdentifier])
            {
                if (([System.Security.Principal.SecurityIdentifier]$identity).AccountDomainSid)
                {
                    $domainObject = Get-DomainData -Domain ([System.Security.Principal.SecurityIdentifier]$identity).AccountDomainSid
                    if ($domainObject)
                    {
                        $domainFQDN = $domainObject.Fqdn
                        $domainName = $domainObject.Name
                    }
                }
            }
            elseif ($identity -like "*\*")
            {
                try { $domainObject = Get-DomainData -Domain $identity.Split("\")[0] -ErrorAction Stop }
                catch { }
                if ($domainObject)
                {
                    $domainFQDN = $domainObject.Fqdn
                    $domainName = $domainObject.Name
                }
            }
            $rootDomain = (Get-ADForest -Server $domainFQDN).RootDomain
            #endregion Resolve Principal Domain
            
            if (-not $script:principals[$domainFQDN]) { $script:principals[$domainFQDN] = @{ } }
            
            # Return form Cache if available
            if ($script:principals[$domainFQDN][$identity])
            {
                return $script:principals[$domainFQDN][$identity]
            }
            
            #region Resolve User in AD
            if ($identity -as [System.Security.Principal.SecurityIdentifier])
            {
                $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(objectSID=$identity)" -Properties ObjectSID, SamAccountName
                # Handle Builtin SIDs that only exist in the root domain
                if (-not $adObject) { $adObject = Get-ADObject -Server $rootDomain -LDAPFilter "(objectSID=$identity)" -Properties ObjectSID, SamAccountName }
            }
            elseif (Test-IsDistinguishedName -Name $identity)
            {
                $adObject = Get-ADObject -Server ($identity | ConvertTo-DnsDomainName) -Identity $identity -Properties ObjectSID, SamAccountName
            }
            elseif ($identity -like "*\*")
            {
                try { $sidName = ([System.Security.Principal.NTAccount]$identity).Translate([System.Security.Principal.SecurityIdentifier]) }
                catch
                {
                    Write-Warning "Failed to translate identity: $identity"
                    continue
                }
                try { $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(objectSID=$sidName)" -Properties ObjectSID, SamAccountName -ErrorAction Stop }
                catch { }
                if (-not $adObject)
                {
                    $script:principals[$domainFQDN][$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[$domainFQDN][$identity]
                    continue
                }
            }
            else
            {
                try
                {
                    $sidName = ([System.Security.Principal.NTAccount]$identity).Translate([System.Security.Principal.SecurityIdentifier])
                    if ($sidName.Value -like 'S-1-3-*')
                    {
                        $script:principals[$domainFQDN][$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[$domainFQDN][$identity]
                        continue
                    }
                    $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(objectSID=$sidName)" -Properties ObjectSID, SamAccountName
                }
                catch
                {
                    $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(SamAccountName=$identity)" -Properties ObjectSID, SamAccountName
                }
            }
            if (-not $adObject -or -not $adObject.ObjectSID)
            {
                Write-Warning "Failed to resolve principal: $identity"
                continue
            }
            #endregion Resolve User in AD
            
            $script:principals[$domainFQDN][$identity] = [pscustomobject]@{
                DistinguishedName = $adObject.DistinguishedName
                Name              = $adObject.SamAccountName
                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[$domainFQDN][$identity]
        }
    }
}

function Resolve-DomainMapping {
    <#
    .SYNOPSIS
        Resolves a source domain from a GPO export into domain of the destination domain.
     
    .DESCRIPTION
        Resolves a source domain from a GPO export into domain of the destination domain.
        The mapping data for this is managed by Register-GptDomainMapping.
        Usual source of mapping data is Import-GptDomainData and a scan of the destination forest.
 
        Accepts SID, Fqdn and Netbios Name as input to find the correct domain.
        Uses SID first, then Fqdn and only as a last resort the Netbios name, if all are specified.
 
        It returns an AD Domain object, representing the destination domain the source domain maps to.
        This object can be faked by the user, if manual data sources need to be included,
        but it is assumed, that such an object will also have all the data fields required.
     
    .PARAMETER DomainSid
        SID of the domain from the export source.
     
    .PARAMETER DomainFqdn
        Fqdn of the domain from the export source.
     
    .PARAMETER DomainName
        Name of the domain from the export source.
     
    .EXAMPLE
        PS C:\> Resolve-DomainMapping -DomainSid $identity.DomainSID -DomainFqdn $identity.DomainFqdn -DomainName $identity.DomainName
 
        Resolves the destination domain to map the specified identity to.
        Tries to use SID first, then FQDN and Netbios name only if nothing else worked.
    #>

    [CmdletBinding()]
    param (
        [string]
        $DomainSid,

        [string]
        $DomainFqdn,

        [string]
        $DomainName
    )

    if (-not $script:domainMapping) {
        throw "No domain mappings loaded yet. Run Import-GptDomainData or Register-GptDomainMapping to initialize the domain resolution table."
    }

    if ($DomainSid -and $script:domainMapping.Sid[$DomainSid]) {
        return $script:domainMapping.Sid[$DomainSid]
    }
    if ($DomainFqdn -and $script:domainMapping.FQDN[$DomainFqdn]) {
        return $script:domainMapping.FQDN[$DomainFqdn]
    }
    if ($DomainName -and $script:domainMapping.Name[$DomainName]) {
        return $script:domainMapping.Name[$DomainName]
    }

    throw "No matching domain found! ($DomainSid | $DomainFqdn | $DomainName)"
}

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 Update-NetworkDrive
{
<#
    .SYNOPSIS
        Remaps mapped network drives if needed.
     
    .DESCRIPTION
        Remaps mapped network drives if needed.
        Performs no operation, if no network drives are mapped on a GPO.
        Migration tables do not correctly update mapped drives, unfoortunately.
     
        Requires valid source data to be already imported, for example by running Import-GptDomainData or Import-GptIdentity.
     
    .PARAMETER GpoName
        Name of the GPO to update.
     
    .PARAMETER Domain
        The destination domain into which the GPO has been imported.
     
    .EXAMPLE
        PS C:\> Update-NetworkDrive -GpoName 'Share Y:' -Domain 'contoso.com'
     
        Updates the GPO "Share Y:" for the domain contoso.com, remapping the share from the source domain to the destination domain.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $GpoName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Domain
    )
    
    begin
    {
        try
        {
            $gpoObject = Get-GPO -Domain $Domain -Name $GpoName -ErrorAction Stop
            $destinationDomain = (Get-DomainData -Domain $Domain).ADObject
            $gpoADObject = Get-ADObject -Server $destinationDomain.PDCEmulator -Identity $gpoObject.Path -Properties gPCFileSysPath -ErrorAction Stop
        }
        catch { throw }
        
        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!"
        }
    }
    process
    {
        Write-Verbose "$GpoName : Processing Network Shares"
        $driveXmlPath = Join-Path -Path $gpoADObject.gPCFileSysPath -ChildPath 'User\Preferences\Drives\Drives.xml'
        if (-not (Test-Path -Path $driveXmlPath))
        {
            Write-Verbose "$GpoName : Does not contain Network Shares"
            return
        }
        
        try { $driveString = Get-Content -Path $driveXmlPath -Raw -ErrorAction Stop -Encoding UTF8 }
        catch
        {
            Write-Verbose "$GpoName : Could not access Network Shares file"
            return
        }
        
        $driveStringNew = $driveString.Replace("\\$sourceDomainDNS\", "\\$($destinationDomain.DNSRoot)\").Replace("\\$sourceDomainNetBios\", "\\$($destinationDomain.NetBIOSName)\")
        
        if ($driveStringNew -eq $driveString)
        {
            Write-Verbose "$GpoName : Nothing to remap in the defined shares"
            return
        }
        
        try { Set-Content -Value $driveStringNew -Path $driveXmlPath -Encoding UTF8 -ErrorAction Stop }
        catch { throw }
    }
}

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
        Write-Verbose "Resolved output path to: $resolvedPath"
        
        $gpoObjects = @()
    }
    process
    {
        Write-Verbose "Resolving GPOs to process"
        if (-not $GpoObject)
        {
            $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name
        }
        else
        {
            foreach ($object in $GpoObject)
            {
                $gpoObjects += $object
            }
        }
    }
    end
    {
        Write-Verbose "Exporting GPO Objects"
        $gpoObjects | Export-GptObject -Path $policyFolder.FullName -Domain $Domain
        Write-Verbose "Exporting GP Links"
        Export-GptLink -Path $resolvedPath -Domain $Domain
        Write-Verbose "Exporting GP Permissions"
        $gpoObjects | Export-GptPermission -Path $resolvedPath -Domain $Domain
        Write-Verbose "Exporting WMI Filters"
        $gpoObjects | Export-GptWmiFilter -Path $resolvedPath -Domain $Domain
        Write-Verbose "Exporting Identities"
        Export-GptIdentity -Path $resolvedPath -Domain $Domain -Name $Identity -GpoObject $gpoObjects
        Write-Verbose "Exporting Domain Information"
        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
        $sourceDomain = [pscustomobject]@{
            Domain          = $Domain
            DomainDNSName = $domainObject.DNSRoot
            NetBIOSName   = $domainObject.NetBIOSName
            BackupVersion = '1.0.0'
            Timestamp      = (Get-Date)
            DomainSID      = $domainObject.DomainSID.Value
        }

        $forestObject = Get-ADForest -Server $Domain
        $domains = $forestObject.Domains | Foreach-Object { Get-ADDomain -Server $_ -Identity $_ } | ForEach-Object {
            [PSCustomObject]@{
                DistinguishedName = $_.DistinguishedName
                Name              = $_.Name
                SID                  = $_.DomainSID
                Fqdn              = $_.DNSRoot
                ADObject          = $_
                IsTarget          = $_.DomainSID -eq $sourceDomain.DomainSID
                IsRootDomain      = $_.DNSRoot -eq $forestObject.RootDomain
            }
        }

        [PSCustomObject]@{
            SourceDomain = $sourceDomain
            ForestDomains = $domains
        } | 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.
 
    .PARAMETER GpoName
        The name filter pattern of the GPOs to parse for relevant identities export.
         
    .PARAMETER GpoObject
        Specific GPO object to parse for relevant identities to export.
     
    .EXAMPLE
        PS C:\> Export-GptIdentity -Path '.'
     
        Export the builtin accounts into the current folder.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string[]]
        $Name,

        [string[]]
        $GpoName = '*',
        
        [Parameter(ValueFromPipeline = $true)]
        $GpoObject,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator
        $rootDomain = Get-ADDomain (Get-ADForest -Server $Domain).RootDomain
        
        [System.Collections.ArrayList]$identities = @()
        
        #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 = '500', '501', '502', '512', '513', '514', '515', '516', '517','520', '521', '522', '525', '526', '553', '571', '572'
        $builtInForestRID = @(
            '498' # Enterprise Read-only Domain Controllers
            '518' # Schema Admins
            '519' # Enterprise Admins
            '527' # Enterprise Key Admins
        )
        $domainSID = (Get-ADDomain -Server $pdcEmulator).DomainSID.Value
        $rootDomainSID = $rootDomain.DomainSID.Value
        $identities.AddRange(($builtInSID | Resolve-ADPrincipal -Domain $Domain))
        $identities.AddRange(($builtInRID | Resolve-ADPrincipal -Domain $Domain -Name { '{0}-{1}' -f $domainSID, $_ }))
        $identities.AddRange(($builtInForestRID | Resolve-ADPrincipal -Domain $rootDomain.DNSRoot -Name { '{0}-{1}' -f $rootDomainSID, $_ }))
        #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
    }
    process
    {
        #region Process GPO-Required Accounts
        foreach ($gpoItem in $GpoObject) {
            foreach ($principal in (Get-GptPrincipal -Name $GpoName -GpoObject $GpoObject -Domain $Domain)) {
                $null = $identities.Add($principal)
            }
        }
        #endregion Process GPO-Required 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)
            {
                # Skip empty lines
                if (-not $link) { continue }
                $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.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [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, @{ Name = 'WmiFilter'; Expression = { $_.WmiFilter.Name }} | 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.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [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 } }
        $select_DomainFqdn = @{ name = 'DomainFqdn'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).DomainFqdn } }
        $select_DomainName = @{ name = 'DomainName'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).DomainName } }

        [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, $select_DomainFqdn, $select_DomainName
        }
        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.
        By default, all filters are exported.
 
        Use -ConstrainExport parameter to switch this behavior to:
        WMI Filters to export are picked up by the GPO they are assigned to.
        Unassigned filters are ignored.
     
    .PARAMETER Path
        The path where to create the export.
        Must be an existing folder.
 
    .PARAMETER ConstrainExport
        Don't export all WMI filters, instead:
        WMI Filters to export are picked up by the GPO they are assigned to.
        Unassigned filters are ignored.
     
    .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,

        [switch]
        $ConstrainExport,
        
        [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 {
        if (-not $ConstrainExport) { return }

        $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 {
        if ($ConstrainExport) {
            $foundFilterHash.Values | Where-Object { $_ } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_wmifilters_$($Domain).csv") -Encoding UTF8 -NoTypeInformation
        }
        else {
            $allFilterHash.Values | Where-Object { $_ } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_wmifilters_$($Domain).csv") -Encoding UTF8 -NoTypeInformation
        }
    }
}


function Get-GptPrincipal
{
<#
    .SYNOPSIS
        Generates a list of principals relevant to the specified GPO.
     
    .DESCRIPTION
        Generates a list of principals relevant to the specified GPO.
        This is used internally to generate the identities export.
        It can also be used directly, to assess needed identities (for example when setting up a test domain).
     
    .PARAMETER Path
        Path to an already existing GPO backup.
        Using this will have the module scan a backup, rather than live GPO.
     
    .PARAMETER Name
        The name to filter GPOs by.
        Defaults to '*'
        Accepts multiple strings, a single wildcard match is needed for a GPO to be selected.
     
    .PARAMETER GpoObject
        The GPO to process, as returned by Get-Gpo.
     
    .PARAMETER Domain
        The domain to connect to.
        Defaults to the user dns domain.
     
    .PARAMETER IncludeUNC
        By default, UNC paths are not included in the output.
        These too can be read from GPO and might be relevant.
     
    .EXAMPLE
        PS C:\> Get-GptPrincipal
     
        Returns the relevant principals from all GPOs in the current domain.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding(DefaultParameterSetName = 'GPO')]
    param (
        [Parameter(ParameterSetName = "Path")]
        [ValidateScript({ Test-Path -Path $_ })]
        [string]
        $Path,
        
        [Parameter(ParameterSetName = 'GPO')]
        [string[]]
        $Name = '*',
        
        [Parameter(ParameterSetName = 'GPO', ValueFromPipeline = $true)]
        $GpoObject,
        
        [string]
        $Domain = $env:USERDNSDOMAIN,
        
        [switch]
        $IncludeUNC
    )
    
    begin
    {
        if (-not $Path)
        {
            $tempPath = New-Item -Path $env:TEMP -ItemType Directory -Name "Gpo_TempBackup_$(Get-Random -Maximum 999999 -Minimum 100000)" -Force
            $backupPath = $tempPath.FullName
        }
        else { $backupPath = (Resolve-Path -Path $Path).ProviderPath }
        
        $entryType = @{
            0 = 'User'
            1 = 'Computer'
            2 = 'LocalGroup'
            3 = 'DomainGroup'
            4 = 'UniversalGroup'
            5 = 'UNCPath'
            6 = 'Unknown'
        }
    }
    process
    {
        #region Export GPO to temporary path
        if (-not $Path)
        {
            $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 $backupPath
        }
        #endregion Export GPO to temporary path
    }
    end
    {
        $groupPolicyManager = New-Object -ComObject GPMgmt.GPM
        $migrationTable = $groupPolicyManager.CreateMigrationTable()
        $constants = $groupPolicyManager.getConstants()
        $backupDirectory = $groupPolicyManager.GetBackupDir($backupPath)
        $backupList = $backupDirectory.SearchBackups($groupPolicyManager.CreateSearchCriteria())
        
        foreach ($policyBackup in $backupList)
        {
            $migrationTable.Add(0, $policyBackup)
            $migrationTable.Add($constants.ProcessSecurity, $policyBackup)
        }
        
        foreach ($entry in $migrationTable.GetEntries())
        {
            $paramAddMember = @{
                MemberType = 'NoteProperty'
                Name       = 'EntryType'
                Value       = $entryType[$entry.EntryType]
                PassThru   = $true
                Force       = $true
            }
            
            switch ($entry.EntryType)
            {
                $constants.EntryTypeUNCPath
                {
                    if (-not $IncludeUNC) { break }
                    
                    [PSCustomObject]@{
                        EntryType = $entryType[$entry.EntryType]
                        Path      = $entry.Source
                    }
                }
                default
                {
                    #region SID
                    if ($sid = $entry.Source -as [System.Security.Principal.SecurityIdentifier])
                    {
                        if ($sid.DomainSID)
                        {
                            Resolve-ADPrincipal -Name $sid -Domain $sid.DomainSID | Add-Member @paramAddMember
                            continue
                        }
                        
                        Resolve-ADPrincipal -Name $sid -Domain $Domain | Add-Member @paramAddMember
                        continue
                    }
                    #endregion SID
                    
                    #region Name
                    try
                    {
                        $sid = ([System.Security.Principal.NTAccount]$entry.Source).Translate([System.Security.Principal.SecurityIdentifier])
                        
                        if ($sid.DomainSID)
                        {
                            Resolve-ADPrincipal -Name $sid -Domain $sid.DomainSID | Add-Member @paramAddMember
                            continue
                        }
                        
                        Resolve-ADPrincipal -Name $sid -Domain $Domain | Add-Member @paramAddMember
                        continue
                    }
                    catch
                    {
                        if ($entry.Source -like '*@*')
                        {
                            $entity, $domainName = $entry.Source -split '@'
                            Resolve-ADPrincipal -Name $entity -Domain $domainName | Add-Member @paramAddMember
                            continue
                        }
                        else
                        {
                            Resolve-ADPrincipal -Name $entry.Source -Domain $Domain | Add-Member @paramAddMember
                            continue
                        }
                    }
                    #endregion Name
                }
            }
        }
        
        if (-not $Path)
        {
            Remove-Item -Path $tempPath -Recurse -Force
        }
    }
}

function Import-GptDomainData
{
<#
    .SYNOPSIS
        Imports domain information of the source domain.
     
    .DESCRIPTION
        Imports domain information of the source domain.
        Also responsible for mapping domains from the source forest to the destination forest.
     
    .PARAMETER Path
        The path to the file or the folder it resides in.
 
    .PARAMETER Domain
        The domain into which to import.
        Used for automatically calculating domain mappings.
     
    .EXAMPLE
        PS C:\> Import-GptDomainData -Path '.'
     
        Import the domain information file from the current folder.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,
        
        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    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
    {
        $domainImport = Import-Clixml $resolvedPath
        $script:sourceDomainData = $domainImport.SourceDomain

        $forestObject = Get-ADForest -Server $Domain
        $targetDomain = Get-ADDomain -Server $Domain
        $domains = $forestObject.Domains | Foreach-Object { Get-ADDomain -Server $_ -Identity $_ } | ForEach-Object {
            [PSCustomObject]@{
                DistinguishedName = $_.DistinguishedName
                Name              = $_.Name
                SID                  = $_.DomainSID
                Fqdn              = $_.DNSRoot
                ADObject          = $_
                IsTarget          = $_.DomainSID -eq $targetDomain.DomainSID
                IsRootDomain      = $_.DNSRoot -eq $forestObject.RootDomain
            }
        }

        foreach ($domainItem in $domains) {
            foreach ($sourceDomainEntry in $domainImport.ForestDomains) {
                if ($sourceDomainEntry.Name -eq $domainItem.Name) {
                    Register-GptDomainMapping -SourceName $sourceDomainEntry.Name -SourceFQDN $sourceDomainEntry.Fqdn -SourceSID $sourceDomainEntry.SID -Destination $domainItem.ADObject
                }
            }
        }
        foreach ($domainItem in $domains) {
            foreach ($sourceDomainEntry in $domainImport.ForestDomains) {
                if ($sourceDomainEntry.Fqdn -eq $domainItem.Fqdn) {
                    Register-GptDomainMapping -SourceName $sourceDomainEntry.Name -SourceFQDN $sourceDomainEntry.Fqdn -SourceSID $sourceDomainEntry.SID -Destination $domainItem.ADObject
                }
            }
        }
        foreach ($domainItem in $domains) {
            foreach ($sourceDomainEntry in $domainImport.ForestDomains) {
                if ($sourceDomainEntry.SID -eq $domainItem.SID) {
                    Register-GptDomainMapping -SourceName $sourceDomainEntry.Name -SourceFQDN $sourceDomainEntry.Fqdn -SourceSID $sourceDomainEntry.SID -Destination $domainItem.ADObject
                }
            }
        }
        $sourceDomain = $domainImport.ForestDomains | Where-Object IsTarget
        $sourceForestRootDomain = $domainImport.ForestDomains | Where-Object IsRootDomain
        foreach ($domainItem in $domains) {
            if ($domainItem.IsRootDomain) {
                Register-GptDomainMapping -SourceName $sourceForestRootDomain.Name -SourceFQDN $sourceForestRootDomain.Fqdn -SourceSID $sourceForestRootDomain.SID -Destination $domainItem.ADObject
            }
        }
        foreach ($domainItem in $domains) {
            if ($domainItem.IsTarget) {
                Register-GptDomainMapping -SourceName $sourceDomain.Name -SourceFQDN $sourceDomain.Fqdn -SourceSID $sourceDomain.SID -Destination $domainItem.ADObject
            }
        }
    }
}

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)" }
        
        $rootDomain = (Get-ADForest -Server $Domain).RootDomain

        # 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 }
        }
        $select_TargetDomain = @{
            Name = 'TargetDomain'
            Expression = { $domainObject }
        }
    }
    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
                    {
                        $adObject = Get-ADObject -Server $rootDomain -LDAPFilter "(objectSID=$($importEntry.SID))" -Properties Name
                        if (-not $adObject) {
                            Write-Warning "Failed to translate identity: $($importEntry.Name) ($($importEntry.SID))"
                            continue
                        }
                        $targetName = $adObject.Name
                    }
                    $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName))
                }
                #endregion Case: Native BuiltIn Principal

                #region Case: Domain Specific BuiltIn Principal
                elseif ($importEntry.IsBuiltIn -eq 'True')
                {
                    try { $domainObject = Resolve-DomainMapping -DomainSid ($importEntry.SID -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid.Value -DomainFqdn $importEntry.DomainFqdn -DomainName $importEntry.DomainName }
                    catch { throw "Cannot resolve domain $($importEntry.DomainFqdn) for $($importEntry.Group) $($importEntry.Name)! $_" }

                    $targetSID = '{0}-{1}' -f $domainObject.DomainSID, $importEntry.RID
                    $adObject = Get-ADObject -Server $domainObject.DNSRoot -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, $select_TargetDomain))
                }
                #endregion Case: Domain Specific BuiltIn Principal
                
                #region Case: Custom Principal
                else
                {
                    try { $domainObject = Resolve-DomainMapping -DomainSid ($importEntry.SID -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid.Value -DomainFqdn $importEntry.DomainFqdn -DomainName $importEntry.DomainName }
                    catch { throw "Cannot resolve domain $($importEntry.DomainFqdn) for $($importEntry.Group) $($importEntry.Name)! $_" }

                    $adObject = Get-ADObject -Server $domainObject.DNSRoot -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, $select_TargetDomain))
                }
                #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.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [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
                {
                    if ($_.Exception.InnerException.HResult -eq 0x800700B7)
                    {
                        New-ImportResult -Action 'Importing Group Policy Links' -Step 'Applying Link: Already Exists' -Target $linkItem.GpoName -Data $linkItem -Success $true -ErrorData $_
                    }
                    else
                    {
                        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
                }
                # Mapped network drives are not correctly covered by Migration Tables
                Update-NetworkDrive -GpoName $gpoEntry.DisplayName -Domain $Domain
                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.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [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
            {
                New-ImportResult -Action 'Update Gpo Permission' -Step 'Apply File Permission' -Target $Permission.GpoName -Success $false -Data $Permission, $accessRule -ErrorData $_
                continue
            }
            #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-GptIdentity 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 Register-GptDomainMapping {
    <#
    .SYNOPSIS
        Maps source domain names to the associated target domain.
     
    .DESCRIPTION
        Maps source domain names to the associated target domain.
        This is used to map source identities to the correct destination domains.
        This data is used during import/restore only!
     
    .PARAMETER SourceName
        Netbios name of the source domain.
        Last resort for source identity domain translation.
     
    .PARAMETER SourceFQDN
        FQDN of the source domain.
     
    .PARAMETER SourceSID
        SID of the source domain.
        Primary tool for source identity domain translation.
     
    .PARAMETER Destination
        The destination domain.
        Either offer an active directory domain object (Returned by Get-ADDOmain) or a name that will be looked up.
     
    .PARAMETER Server
        Server to use for looking up the destination domain data.
        Used only when the Destination parameter waas set to string value (such as the fqdn of the domain).
     
    .EXAMPLE
        PS C:\> Register-GptDomainMapping -SourceName corp -SourceFQDN corp.contoso.com -SourceSID $sid -Destination $domain
 
        Registers name mappings, pointing corp.contoso.com to the destination domain stored in $domain.
    #>

    [CmdletBinding()]
    param (
        [string]
        $SourceName,

        [string]
        $SourceFQDN,

        [string]
        $SourceSID,

        $Destination,
        [string]
        
        $Server
    )

    begin
    {
        if (-not $script:domainMapping) {
            $script:domainMapping = @{
                Name = @{ }
                FQDN = @{ }
                SID = @{ }
            }
        }
        # Do not check for actual type, in order to allow users to fake/mock up a custom object
        if ($Destination.PSObject.TypeNames -contains 'Microsoft.ActiveDirectory.Management.ADDomain') {
            $domainObject = $Destination
        }
        else {
            $params = @{
                Domain = $Destination
                ErrorAction = 'Stop'
            }
            if ($Server) { $params['Server'] = $Server }
            try { $domainObject = Get-ADDomain @params }
            catch {
                Write-Warning "Failed to resolve destination domain: $Destination : $_"
                throw
            }
        }
    }
    process
    {
        if ($SourceName) {
            $script:domainMapping.Name[$SourceName] = $domainObject
        }
        if ($SourceFQDN) {
            $script:domainMapping.FQDN[$SourceFQDN] = $domainObject
        }
        if ($SourceSID) {
            $script:domainMapping.SID[$SourceSID] = $domainObject
        }
    }
}

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 @common
    }
    process
    {
        Write-Verbose "Importing Identities"
        Import-GptIdentity @common -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
    }
}

#endregion Load compiled code