ESAE.psm1

$script:LogPath = "$env:SystemDrive\adforest_install.log"

$script:FinishSecureADForestTask = "FinishSecureADForest"
$script:InstallSecureADForestTask = "InstallSecureADForest"

$script:RemoteForestPasswordFilePath = "$env:ALLUSERSPROFILE\Microsoft\RFPassword.txt" 
$script:LocalForestPasswordFilePath = "$env:ALLUSERSPROFILE\Microsoft\LFPassword.txt"
$script:SafeModeAdminPasswordFilePath = "$env:ALLUSERSPROFILE\Microsoft\ADPassword.txt"

$script:ConfigFilePath = "$PSScriptRoot\config.json"
$script:MSSFilePath = "$PSScriptRoot\sceregvl.inf"

$script:BaseGPOBundle = "$PSScriptRoot\BaseGPOBundle.zip"
$script:ForestGPLinks = "$PSScriptRoot\Forest_GPLinks.json"
$script:OUStructure = "$PSScriptRoot\Forest_OU.json"
$script:AdditionalGPOs = "$PSScriptRoot\Forest_AdditionalGPOBundle.zip"
$script:AdditionalGPLinks = "$PSScriptRoot\Forest_AdditionalGPLinks.json"
$script:ForestLocalGroups = "$PSScriptRoot\Forest_LocalGroups.json"
$script:BaseAdmxBundle = "$PSScriptRoot\BaseAdmxBundle.zip"
$script:AddOnAdmxBundle = "$PSScriptRoot\AddOnAdmxBundle.zip"
$script:SysvolFiles = [System.IO.Path]::Combine($PSScriptRoot, "SysvolFiles.zip")

$script:ESAEOUConfigFilePath = "$PSScriptRoot\ESAE_OU.json"
$script:ESAELocalGroups = "$PSScriptRoot\ESAE_LocalGroups.json" #Not really needed, the script will use the default groups and name them appropriately
$script:ESAERemoteGroups = "$PSScriptRoot\ESAE_RemoteManagementGroups.json"
$script:ESAEGPLinks = "$PSScriptRoot\ESAE_GPLinks.json"

#region Base Commands

Function New-SecureADForest {
        <#
        .SYNOPSIS
            The cmdlet builds a new AD forest with a single domain on a single server, creates standard user groups and OUs, imports pre-configured GPOs with the DISA STIGs, and runs AD and DNS STIGs.
 
        .DESCRIPTION
            The cmdlet renames the computer, if desired, sets the local administrator password, and then installs Active Directory with a new forest and domain. It sets up an AD site, and then sets up some AD
            infrastructure from configuration files or uses a default setup to include an OU structure, security groups, and user accounts. DISA STIG GPOs are imported and linked to OUs based on a configuration
            file. If this forest is designated as an Enhanced Security Administrative Environment (ESAE), it sets up conditional forwarders on the DC and the remote forest if specified, and then builds a one way
            forest trust. The AD recycle bin is enabled, the Group Policy Central Store is setup, and the MSS GPO settings are imported to be available to use.
 
            This cmdlet takes the parameter input and creates a config file that is used by later steps that are enacted through scheduled tasks. All passwords are temporarily stored on the file system encrypted
            with SYSTEM credentials and deleted.
 
            This cmdlet specifically creates the config file from the parameter and saves it, renames the computer, sets the administrator password, and saves the encrypted passwords that are used later. Then,
            if a reboot is required due to renaming the computer, it creates a scheduled task to run Install-SecureADForest, otherwise the next cmdlet is run directly without a reboot.
 
        .PARAMETER ForestRootDomainName
            The name of the forest and root domain.
 
        .PARAMETER ComputerName
            The new name the domain controller should have.
 
        .PARAMETER AdminPassword
            The new password for the local administrator account.
 
        .PARAMETER BaseNTDSPath
            The root location to place the NTDS.dit, SYSVOL, and logs file folders. Useful if the Active Directory files contents are being placed on a separate drive from the OS. This defaults to c:\NTDS.
 
        .PARAMETER FunctionalLevel
            The functional level of the forest and root domain. This defaults to 6.
 
            -- Windows Server 2003 : 2
            -- Windows Server 2008 : 3
            -- Windows Server 2008 R2 : 4
            -- Windows Server 2012 : 5
            -- Windows Server 2012 R2 : 6
 
        .PARAMETER SiteName
            The name of the AD site the new domain controller will be a part of. The defaults to Default-First-Site-Name.
 
        .PARAMETER SiteSubnets
            The subnet blocks that will be added to the site. These should be in the format of X.X.X.X/CIDR.
 
        .PARAMETER SafeModePassword
            The DSRM password. This will default to the AdminPassword if it is specified, or will prompt for input if it is not.
 
        .PARAMETER LogPath
            The path progress logs are written to. This defaults to $env:SystemDrive\adforest_install.log.
 
        .PARAMETER OUConfigFilePath
            The file containing the OU setup configuration. If no file is specified, a default OU structure is implemented.
 
        .PARAMETER ForestGroupsConfigFilePath
            The file containing the security group configuration for the groups that are used to manage the forest. If no file is specified, a default set of administrative groups are deployed. The groups are added
            to existing built in groups, like Domain Admins, so that well-known SID groups are not utilized for the membership of user accounts.
 
            If a custom OU structure is specified, this configuration must be specified so that the groups are created in OUs that actually exist.
 
        .PARAMETER IsESAEForest
            Specify that this forest will be an ESAE forest. This option prompts additional available dynamic parameters needed to configure the ESAE forest and forest trust.
 
        .PARAMETER GPOBundleFilePath
            The path to the default set of GPOs that include the DISA STIGs. This defaults to $PSScriptRoot\BaseGPOBundle.zip. The default bundle of GPOs utilize WMI filters that are also imported.
 
        .PARAMETER GPLinksFilePath
            The path to the configuration file that maps the imported base GPOs specified in GPOBundleFilePath to OUs. This will default to "$PSScriptRoot\Forest_GPLinks.json" for a standalone forest install and
            default to "$PSScriptRoot\ESAE_GPLinks.json" for an ESAE installation.
     
        .PARAMETER AdditonalGPOsFilePath
            The path to a zip file containing any additional GPOs that should be imported. This is useful so that the base GPO bundle can contain just the STIG GPOs, and this can contain environment specific
            GPOs. If no file is specified, no additional GPOs are imported.
     
        .PARAMETER AdditionalGPLinksFilePath
            The path to the configuration file that maps the imported additional GPOs specified in AdditionalGPOsFilePath to OUs.
 
        .PARAMETER BaseAdmxBundleFilePath
            The path to the zip file with the ADMX and ADML files that will be added to the central policy store. This defaults to "$PSScriptRoot\BaseAdmxBundle.zip". To use the local PolicyDefinitions folder, set this parameter as an empty string.
     
        .PARAMETER AddOnAdmxBundleFilePath
            The path to the zip file with additional ADMX and ADML files. This is useful so that the base bundle can remain unchanged and the add-on can be environment specific.
 
        .PARAMETER SysvolFilesPath
            The path to the zip containing files and folders that will be extracted to the SYSVOL into a folder named "files". This defaults to "$PSScriptRoot\Sysvolfiles.zip".
 
        .PARAMETER UsersFilePath
            The path to the configuration file that specifies users to be created and their group membership. This defaults to none.
     
        .PARAMETER AdditionalSitesConfigFilePath
            The path to the configuration file that specifies additional AD sites and subnets to be created.
 
        .PARAMETER RemoteForestCredential
            The credentials to use to connect to the remote forest during an ESAE installation to create the conditional forwarder pointing to the ESAE forest and setup the forest trust.
 
        .PARAMETER RemoteForest
            The DNS name of the remote forest during an ESAE installation.
 
        .PARAMETER LocalForest
            The DNS name of the local forest being setup for ESAE. This is only required if the forest being setup is different than the local forest the cmdlet is running on. Typically this is not required.
 
        .PARAMETER LocalForestCredential
            Specifies the credentials to use on the local forest to set up the forest trust for an ESAE forest. This is only needed if the specific credentials are needed in the local forest, typically not required.
     
        .PARAMETER ConfigFilePath
            The path to the configuration file containing all of the required parameters to execute the cmdlet. This can be used to help programatically execute the cmdlet repeatedly, but this cmdlet will still
            need to have the AdminPassword, SafeModePassword, or both specified directly. They can be stored as secure strings in the config file, but that is not really secure.
 
        .PARAMETER CreateRemoteConditionalForwarder
            Specify that a conditional forwarder to the new ESAE forest should be created in the remote forest. Requires RemoteForestCredential is specified.
 
        .PARAMETER CreateLocalConditionalForwarder
            Specify that a conditional forwarder to the remote forest should be created in the new ESAE forest. This needs to be specified unless an external DNS solution is utilized to resolve the remote forest.
 
        .PARAMETER RemoteForestMasterServers
            The remote DNS servers that will be used to create the local conditional forwarder in the new ESAE forest.
 
        .PARAMETER ManagementGroupsConfigFilePath
            The path to the configuration file that specifies groups to be created that will be used to manage remote forests and domains from the new ESAE forest.
 
        .INPUTS
            System.String
            This represents the configuration file path.
 
        .OUTPUTS
            None
 
        .EXAMPLE
            New-SecureADForest -ForestRootDomainName "admin.local" `
                                -ComputerName "AdminDC" `
                                -BaseNTDSPath "n:\NTDS" `
                                -SiteName "Site1" `
                                -SiteSubnets @("192.168.1.0/24") `
                                -GPOBundleFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\BaseGPOBundle.zip" `
                                -GPLinksFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_GPLinks.json" `
                                -OUConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_OU.json" `
                                -ManagementGroupsConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_RemoteManagementGroups.json" `
                                -ForestGroupsConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_LocalGroups.json" `
                                -UsersFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\users.json" `
                                -SysvolFilesPath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_Sysvolfiles.zip" `
                                -IsESAEForest `
                                -RemoteForest "contoso.com" `
                                -RemoteForestMasterServers @("192.168.2.1") `
                                -CreateLocalConditionalForwarder `
                                -RemoteForestCredential (Get-Credential) `
                                -CreateRemoteConditionalForwarder `
                                -AdditionalGPOsFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_AdditionalGPOBundle.zip" `
                                -AdditionalGPLinksFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_AdditionalGPLinks.json" `
                                -BaseAdmxBundleFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\BaseAdmxBundle.zip" `
                                -AddOnAdmxBundleFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\AddOnAdmxBundle.zip"
 
            Builds a new ESAE forest including the defined OU structure, remote forest management groups, specified users, and DNS settings to administer the contoso.com forest from the new admin.local forest.
 
            The default security groups for managing the ESAE forest are used. The OU.json file uses an OU structure compatible with the specified security group creation locations.
 
        .EXAMPLE
            New-SecureADForest -ForestRootDomainName "admin.local" `
                                -ComputerName "AdminDC" `
                                -BaseNTDSPath "n:\NTDS" `
                                -SiteName "Site1" `
                                -SiteSubnets @("192.168.1.0/24") `
                                -ManagementGroupsConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_RemoteManagementGroups.json" `
                                -UsersFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\users.json" `
                                -IsESAEForest `
                                -RemoteForest "contoso.com" `
                                -RemoteForestMasterServers @("192.168.2.1") `
                                -CreateLocalConditionalForwarder `
                                -RemoteForestCredential (Get-Credential) `
                                -CreateRemoteConditionalForwarder `
                                -AdditionalGPOsFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_AdditionalGPOBundle.zip" `
                                -AdditionalGPLinksFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_AdditionalGPLinks.json" `
                                -AddOnAdmxBundleFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\AddOnAdmxBundle.zip"
 
            Builds a new ESAE forest including the defined OU structure, remote forest management groups, specified users, and DNS settings to administer the contoso.com forest from the new admin.local forest.
 
            The default security groups for managing the ESAE forest are used, as well as the default OU structure. The defaults for the base GPO Bundle, the base Admx Bundle, and the GP Links are used as well.
 
        .EXAMPLE
            New-SecureADForest -ForestRootDomainName "admin.local" `
                                -ComputerName "AdminDC" `
                                -BaseNTDSPath "n:\NTDS" `
                                -SiteName "Site1" `
                                -SiteSubnets @("192.168.1.0/24") `
                                -OUConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\Forest_OU.json" `
                                -ForestGroupsConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\Forest_LocalGroups.json" `
                                -UsersFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\users.json" `
                                -AdditionalGPOsFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\Forest_AdditionalGPOBundle.zip" `
                                -AdditionalGPLinksFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\Forest_AdditionalGPLinks.json"
 
            Builds a new standalone secure forest including the defined OU structure, and specified users.
 
        .EXAMPLE
            New-SecureADForest -ForestRootDomainName "admin.local"
 
            This represents the minimum configuration to deploy a new forest called admin.local. All of the default OU and group settings are used. No users are created and no additional GPOs or ADMX files are imported. The default Sysvol files
            are extracted. No AD Site information is updated. The base GPO bundle and associated links are used.
 
        .NOTES
            The remote forest credentials should have Enterprise Admin permissions in the remote forest.
    #>

    [CmdletBinding(DefaultParameterSetName="File")]
    Param(
        [Parameter(Mandatory=$true, ParameterSetName="Parameters")]
        [string]$ForestRootDomainName,
        [Parameter(ParameterSetName="Parameters")]
        [string]$ComputerName,
        [Parameter()]
        [SecureString]$AdminPassword,
        [Parameter(ParameterSetName="Parameters")]
        [string]$BaseNTDSPath = "c:\NTDS",
        [Parameter(ParameterSetName="Parameters")]
        [ValidateRange(2,6)]
        [int]$FunctionalLevel = 6,
        [Parameter(ParameterSetName="Parameters")]
        [string]$SiteName = "Default-First-Site-Name",
        [Parameter(ParameterSetName="Parameters")]
        [string[]]$SiteSubnets = @(),
        [Parameter()]
        [SecureString]$SafeModePassword,
        [Parameter(ParameterSetName="Parameters")]
        [string]$LogPath = $script:LogPath,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$OUConfigFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$ForestGroupsConfigFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [switch]$IsESAEForest,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$GPOBundleFilePath = $script:BaseGPOBundle,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]    
        [string]$GPLinksFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]    
        [string]$AdditionalGPOsFilePath,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]        
        [string]$AdditionalGPLinksFilePath,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$BaseAdmxBundleFilePath = $script:BaseAdmxBundle,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$AddOnAdmxBundleFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$UsersFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$AdditionalSitesConfigFilePath = [System.String]::Empty,
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$SysvolFilesPath = $script:SysvolFiles,
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="File")]
        [ValidateScript({Test-Path -Path $_})]    
        [string]$ConfigFilePath = $script:ConfigFilePath
    )
    DynamicParam
    {
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        $ValidateScript = New-Object -TypeName System.Management.Automation.ValidateScriptAttribute([System.Management.Automation.ScriptBlock]::Create("if (![System.String]::IsNullOrEmpty(`$_)) { Test-Path -Path `$_ } else { return `$true }"))

        if ($IsESAEForest) {

            [System.Management.Automation.ParameterAttribute]$Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
            $Attributes.ParameterSetName = "Parameters"
            $Attributes.ValueFromPipelineByPropertyName = $true
            
            $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($Attributes)

            #region RemoteForestCredential
            
            $Temp = $AttributeCollection.Remove($Attributes)
            $Attributes.Mandatory = $true
            $AttributeCollection.Add($Attributes)

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("RemoteForestCredential", [PSCredential], $AttributeCollection)
            $ParamDictionary.Add("RemoteForestCredential", $DynParam)
            
            $Temp = $AttributeCollection.Remove($Attributes) 
            $Attributes.Mandatory = $false
            $AttributeCollection.Add($Attributes)

            #endregion

            #region RemoteForest

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("RemoteForest", [string], $AttributeCollection)
            $ParamDictionary.Add("RemoteForest", $DynParam)

            #endregion

            #region RemoteForest

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("LocalForest", [string], $AttributeCollection)
            $ParamDictionary.Add("LocalForest", $DynParam)

            #endregion

            #region LocalForestCredential
     
            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("LocalForestCredential", [PSCredential], $AttributeCollection)
            $ParamDictionary.Add("LocalForestCredential", $DynParam)

            #endregion

            #region ESAE Stub Zones

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("CreateRemoteConditionalForwarder", [switch], $AttributeCollection)
            $ParamDictionary.Add("CreateRemoteConditionalForwarder", $DynParam)

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("CreateLocalConditionalForwarder", [switch], $AttributeCollection)
            $ParamDictionary.Add("CreateLocalConditionalForwarder", $DynParam)

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("RemoteForestMasterServers", [string[]], $AttributeCollection)
            $ParamDictionary.Add("RemoteForestMasterServers", $DynParam)

            #endregion

            #region Remote Forest Management Groups
            
            $AttributeCollection.Add($ValidateScript)
            
            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("ManagementGroupsConfigFilePath", [string], $AttributeCollection)
            $ParamDictionary.Add("ManagementGroupsConfigFilePath", $DynParam)
            
            $Temp = $AttributeCollection.Remove($ValidateScript)

            #endregion
        }

        return $ParamDictionary  
    }

    Begin {

        $PsBoundParameters.GetEnumerator() | ForEach-Object { 
            New-Variable -Name $_.Key -Value $_.Value -ErrorAction SilentlyContinue
        }
        
        switch ($PSCmdlet.ParameterSetName) {
            "Parameters" {

                if ([System.String]::IsNullOrEmpty($LogPath)) {
                    $LogPath = $script:LogPath
                }

                $ParametersNotToSave = @("AdminPassword","SafeModePassword","ConfigFilePath","RemoteForestCredential","LocalForestCredential")
                $CommandName = $PSCmdlet.MyInvocation.InvocationName
                $ParameterList = @{}
         
                (Get-Command -Name $CommandName).Parameters.GetEnumerator() | Select-Object -ExpandProperty Key | Where-Object {$_ -notin $ParametersNotToSave} | ForEach-Object {
                    $Temp = Get-Variable -Name $_ -ErrorAction SilentlyContinue 

                    if ($Temp -ne $null -and $Temp.Name -ne $null) {

                        if ($Temp.Value.GetType() -eq [System.Management.Automation.SwitchParameter]) {
                            $ParameterList.Add($Temp.Name, [System.Boolean]::Parse($Temp.Value.ToString()))
                        }
                        else {
                            $ParameterList.Add($Temp.Name,$Temp.Value)
                        }
                    }
                }

                $PsBoundParameters.GetEnumerator() | Where-Object {$_.Key -notin $ParametersNotToSave } | ForEach-Object {
                    if ($_ -ne $null -and $_.Key -ne $null) {
                        if (!@($ParameterList.Keys).Contains($_.Key)) {
                            if ($_.Value.GetType() -eq [System.Management.Automation.SwitchParameter]) {
                                $ParameterList.Add($_.Key, [System.Boolean]::Parse($_.Value.ToString()))
                            }
                            else {
                                $ParameterList.Add($_.Key, $_.Value)    
                            }                    
                        }
                    }
                }

                if ($IsESAEForest) {
                    $RemoteForestCredential = $PSBoundParameters.RemoteForestCredential
                    $LocalForestCredential = $PSBoundParameters.LocalForestCredential

                    $ParameterList.Add("RemoteForestUserName", $RemoteForestCredential.UserName)
                    $ParameterList.Add("RemoteForestPasswordFilePath", $script:RemoteForestPasswordFilePath)

                    if ($LocalForestCredential -ne $null -and $LocalForestCredential -ne [PSCredential]::Empty) {
                        $ParameterList.Add("LocalForestUserName", $LocalForestCredential.UserName)
                        $ParameterList.Add("LocalForestPasswordFilePath", $script:LocalForestPasswordFilePath)
                    }
                    else {
                        $ParameterList.Add("LocalForestPasswordFilePath", "")
                        $ParameterList.Add("LocalForestUserName", "")
                    }
                }

                $Config = New-ConfigurationFile -ParameterList $ParameterList
                $Parameters = ConvertFrom-Json -InputObject $Config.Content
                
                break
            }
            "File" {
                $Config = New-ConfigurationFile -SourceFilePath $ConfigFilePath
                $Parameters = ConvertFrom-Json -InputObject $Config.Content

                if ([System.String]::IsNullOrEmpty($Parameters.ForestRootDomainName)) {
                    throw "Configuration file missing required parameter ForestRootDomainName."
                }

                if ([System.String]::IsNullOrEmpty($Parameters.LogPath)) {
                    $Parameters.LogPath = $script:LogPath
                }

                if ([System.String]::IsNullOrEmpty($Parameters.FunctionalLevel)) {
                    $Parameters.FunctionalLevel = 6
                }

                if ([System.String]::IsNullOrEmpty($Parameters.BaseAdmxBundleFilePath)) {
                    $Parameters.BaseAdmxBundleFilePath = $script:BaseAdmxBundle
                }

                break
            }
            default {
                throw "Could not determine the parameter set for New-SecureADForest"
            }
        }

        [System.Environment]::SetEnvironmentVariable("NewADForestLogPath", $Parameters.LogPath, [System.EnvironmentVariableTarget]::Machine)
    
        [bool]$Success = $true

        if ($SafeModePassword -eq $null -and $AdminPassword -eq $null) {
            $VerifyPassword = $null
            while(($SafeModePassword -eq $null -or $VerifyPassword -eq $null) -or ($SafeModePassword -ne $VerifyPassword)) {
                $SafeModePassword = Read-Host -AsSecureString -Prompt "Enter the DSRM safe mode password"
                $VerifyPassword = Read-Host -AsSecureString -Prompt "Verify the DSRM safe mode password"
            }
        }

        if ($SafeModePassword -eq $null -and $AdminPassword -ne $null) {
            $SafeModePassword = $AdminPassword
        }

        if (![System.String]::IsNullOrEmpty($Parameters.UsersFilePath)) {
            if (!(Test-Path -Path $Parameters.UsersFilePath)) {
                Write-Warning "The file specified for the user import, $($Parameters.UsersFilePath), could not be found."
                Write-Log "The file specified for the user import, $($Parameters.UsersFilePath), could not be found." 
                throw [System.IO.FileNotFoundException]("The file specified for the user import, $($Parameters.UsersFilePath), could not be found." )
            }
        }

        if (![System.String]::IsNullOrEmpty($Parameters.ForestGroupsConfigFilePath)) {
            if (!(Test-Path -Path $Parameters.ForestGroupsConfigFilePath)) {
                Write-Warning "The file specified for the forest groups, $($Parameters.ForestGroupsConfigFilePath), could not be found."
                Write-Log "The file specified for the forest groups, $($Parameters.ForestGroupsConfigFilePath), could not be found."
                throw [System.IO.FileNotFoundException]("The file specified for the forest groups, $($Parameters.ForestGroupsConfigFilePath), could not be found.")
            }
        }

        if (![System.String]::IsNullOrEmpty($Parameters.OUConfigFilePath)) {
            if(!(Test-Path -Path $Parameters.OUConfigFilePath)) {
                Write-Warning "The file specified for the OU configuration, $($Parameters.OUConfigFilePath), could not be found."
                Write-Log "The file specified for the OU configuration, $($Parameters.OUConfigFilePath), could not be found."
                throw [System.IO.FileNotFoundException]("The file specified for the OU configuration, $($Parameters.OUConfigFilePath), could not be found.")
            }
        }

        Enable-TaskSchedulerHistory 
    }

    Process {

        try
        {
            Write-Log "Adding ADDS windows feature."
            Add-ADDSWindowsFeature -IncludeManagementTools

            try
            {
                if ($AdminPassword -ne $null) {
                    Write-Log "Setting local admin password and enabling the account if disabled."
                    Set-LocalAdminPassword -AdminPassword $AdminPassword -EnableAccount
                }
                else {
                    Write-Log "No new password specified, skipping."
                }
                
                try 
                {
                    $RebootRequired = $false

                    if (![System.String]::IsNullOrEmpty($Parameters.ComputerName)) {
                        Write-Log "Renaming computer to $($Parameters.ComputerName)."
                        Rename-LocalComputer -ComputerName $Parameters.ComputerName -ErrorAction SilentlyContinue
                        $RebootRequired = $true
                    }
                    else {
                        Write-Log "No computer name provided, not renaming."
                    }

                    if ($Parameters.IsESAEForest) {
                        Write-Log "This is an ESAE Forest installation."
                        try
                        {
                            Write-Log "Saving encrypted remote forest password."
                            $Temp = New-SaveEncryptedPasswordTask -Password ($RemoteForestCredential.Password) -FilePath $script:RemoteForestPasswordFilePath
                            Write-Log "Remote forest password saved at $Temp."
                        }
                        catch [Exception] {
                            Write-Log $_
                            $Success = $false
                        }
                        
                        if ($LocalForestCredential -ne $null -and $LocalForestCredential -ne [PSCredential]::Empty) {
                            try
                            {
                                Write-Log "Saving encrypted local forest password."
                                $Temp = New-SaveEncryptedPasswordTask -Password ($LocalForestCredential.Password) -FilePath $script:LocalForestPasswordFilePath
                                Write-Log "Local forest password saved at $Temp."
                            }
                            catch [Exception] {
                                Write-Log $_
                                $Success = $false
                            }
                        }
                    }

                    if ($RebootRequired) {
                        try
                        {
                            Write-Log "Saving encrypted admin password."
                            $Temp = New-SaveEncryptedPasswordTask -Password $SafeModePassword -FilePath $script:SafeModeAdminPasswordFilePath
                            Write-Log "Safe mode password saved at $Temp."

                            try
                            {
                                Write-Log "Preparing Install Forest scheduled task."
                                New-InstallSecureADForestScheduledTask -ConfigFilePath ($Config.FilePath) -PasswordFilePath $script:SafeModeAdminPasswordFilePath
                                Write-Log "Completed building scheduled task."
                            }
                            catch [Exception] {
                                Write-Log $_
                                $Success = $false
                            }
                        }
                        catch [Exception] {
                            Write-Log $_
                            $Success = $false
                        }
                    }
                    else {
                        Write-Log "No reboot required, moving to installing the forest."
                        Install-SecureADForest -ConfigFilePath $Config.FilePath -SafeModePassword $SafeModePassword
                    }            
                }
                catch [Exception] {
                    Write-Log $_
                    $Success = $false
                }
            }
            catch [Exception] {
                Write-Log $_
                $Success = $false
            }
        }
        catch [Exception] {
            Write-Log $_
            $Success = $false
        } 
    }

    End {
        if ($Success -and $RebootRequired) {
            Write-Log "Restarting computer."
            Restart-Computer -Force
        }

        if (!$Success) {
            Write-Log "One of the steps in New-SecureADForest failed."
            $CommandText = Get-CommandText -Command ($PSCmdlet.MyInvocation.MyCommand.Name) -Parameters $PSBoundParameters
            Write-Log "Rerun this command: $CommandText"
        }
    }
}

Function Install-SecureADForest {
    <#
        .SYNOPSIS
            This cmdlet launches the installation of a new, secure AD forest.
 
        .DESCRIPTION
            Active Directory is installed through this cmdlet at a 2012 R2 functional level.
     
            After the installation is complete, if a configuration file was used to initiate the command, a scheduled task is created that will call Set-ADForestSecurityConfiguration after a reboot. If the restart
            parameter is not specified, the server must manually be rebooted to start the next steps. The New-SecureADForest cmdlet specifies that the server should be rebooted when calling this step.
 
            If the cmdlet is not launched from the configuration file input, the cmdlet will optionally reboot the machine based in the inputted parameter.
 
        .PARAMETER ConfigFilePath
            The path to the configuration file with the parameters used to run this cmdlet. This defaults to "$PSScriptRoot\config.json".
 
        .PARAMETER SafeModePassword
            The password to use with DSRM.
 
        .PARAMETER BaseNTDSPath
            The root folder to put the appropriate Active Directory database, log, and sysvol folders and files. This is useful when those files are stored on a drive different than the OS.
 
        .PARAMETER ForestRootDomainName
            The DNS name of the new forest.
 
        .PARAMETER Restart
            Specify whether to automatically restart the server after Active Directory is installed.
 
        .PARAMETER FunctionalLevel
            The functional level of the forest and root domain. This defaults to 6.
 
            -- Windows Server 2003 : 2
            -- Windows Server 2008 : 3
            -- Windows Server 2008 R2 : 4
            -- Windows Server 2012 : 5
            -- Windows Server 2012 R2 : 6
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Install-SecureADForest -ConfigFilePath "$PSScriptRoot\config.json" -Restart
 
            Installs Active Directory based on the config file path and automatically reboots the server after the installation. A scheduled task is created to run the follow on security hardening and configuration
            steps after the reboot is complete.
 
        .EXAMPLE
            Install-SecureADForest -BaseNTDSPath "n:\NTDS" -ForestRootDomainName "admin.local" -Restart -FunctionalLevel 6 -SafeModePassword (ConvertTo-SecureString -String "P@$$w0rd" -AsPlainText -Force)
 
            Installs Active Directory to n:\NTDS in a forest named admin.local with a Server 2012 R2 functional level. The server is automatically rebooted after installation.
         
        .NOTES
            None
    #>

    [CmdletBinding(DefaultParameterSetName="File")]
    Param (
        [Parameter(ParameterSetName="File")]
        [ValidateScript({Test-Path -Path $_})]    
        [string]$ConfigFilePath = $script:ConfigFilePath,
        [Parameter(Mandatory=$true)]
        [SecureString]$SafeModePassword,
        [Parameter(ParameterSetName="Parameters")]
        [string]$BaseNTDSPath = "c:\NTDS",
        [Parameter(Mandatory=$true, ParameterSetName="Parameters")]
        [string]$ForestRootDomainName,
        [Parameter(ParameterSetName="Parameters")]
        [switch]$Restart = $false,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateRange(2,6)]
        [int]$FunctionalLevel = 6
    )

    Begin {
        switch ($PSCmdlet.ParameterSetName) {
            "File" {
                Write-Log "Getting configuration file information."
                $Parameters = ConvertFrom-Json -InputObject (Get-Content -Path $ConfigFilePath -Raw)

                if (!([System.String]::IsNullOrEmpty($Parameters.ForestRootDomainName))) {
                    $ForestRootDomainName = $Parameters.ForestRootDomainName
                }
                else {
                    Write-Log "Forest root domain name is null or empty."
                    throw [System.ArgumentNullException]("Forest root domain name is null or empty")
                }
                
                if (![System.String]::IsNullOrEmpty($Parameters.BaseNTDSPath)) {
                    $BaseNTDSPath = $Parameters.BaseNTDSPath
                }

                if(![System.String]::IsNullOrEmpty($Parameters.FunctionalLevel)) {
                    $FunctionalLevel = $Parameters.FunctionalLevel
                }

                break
            }
            "Parameters" {

                break
            }
        }

        [bool]$Success = $false

        Write-Log "Checking for Install AD Forest Scheduled Task."

        if ((Get-ScheduledTask -TaskName $script:InstallSecureADForestTask -ErrorAction SilentlyContinue) -ne $null) {
            Write-Log "Removing Install AD Forest Scheduled Task."
            Unregister-ScheduledTask -TaskName $script:InstallSecureADForestTask -Confirm:$false
        }

        $Netbios = $ForestRootDomainName.Substring(0,[System.Math]::Min($ForestRootDomainName.IndexOf("."), 15))
    }

    Process {

        <#
            -- Windows Server 2003 : 2 or Win2003
            -- Windows Server 2008 : 3 or Win2008
            -- Windows Server 2008 R2 : 4 or Win2008R2
            -- Windows Server 2012 : 5 or Win2012
            -- Windows Server 2012 R2 : 6 or Win2012R2
        #>


        try
        {
            Write-Log "Installing AD Forest."

            Install-ADDSForest `
                -CreateDnsDelegation:$false `
                -DatabasePath "$BaseNTDSPath\Database" `
                -DomainMode 6 `
                -DomainName $ForestRootDomainName `
                -DomainNetbiosName $Netbios `
                -ForestMode 6 `
                -InstallDns:$true `
                -LogPath "$BaseNTDSPath\Logs" `
                -NoRebootOnCompletion:$true `
                -SysvolPath "$BaseNTDSPath\SYSVOL" `
                -Force:$true `
                -SafeModeAdministratorPassword $SafeModePassword


            if ($PSCmdlet.ParameterSetName -eq "File") {
                Write-Log "Preparing finish AD Forest installation scheduled task."
                New-FinishSecureADForestInstallationScheduledTask -ConfigFilePath $ConfigFilePath
                Write-Log "Completed building the scheduled task."
            }

            $Success = $true
        }
        catch [Exception] 
        {
            Write-Log $_
        }
    }   

    End {
        if ($Success) {
            Write-Log "Installation completed successfully."
            if (($PSCmdlet.ParameterSetName -eq "Parameters" -and $Restart) -or ($PSCmdlet.ParameterSetName -eq "File")) {
                Write-Log "Restarting computer."
                Restart-Computer -Force
            }
            else {
                Write-Log "Restart automatically not selected. The server needs to be rebooted to finish the installation."
            }
        }
        else {
            Write-Log "One of the steps in Install-SecureADForest failed."
            $CommandText = Get-CommandText -Command ($PSCmdlet.MyInvocation.MyCommand.Name) -Parameters $PSBoundParameters
            Write-Log "Rerun this command: $CommandText"
        }
    }
}

Function Set-ADForestSecurityConfiguration {
    <#
        .SYNOPSIS
            This cmdlet completes the installation of Active Directory and sets default configurations and implements security hardening.
 
        .DESCRIPTION
            This cmdlet renames the default site to the specified name, adds the specified subnets to the site, creates any additional specified sites, installs the KDS root key with an immediate effective time,
            enables the AD recycle bin, adds the MSS GPO settings, creates the specified OU structure, and creates groups to manage the new forest.
 
            If this is an ESAE forest, additional groups are also created for managing remote forests, if specified.
 
            Then the central policy store is created and the ADMX bundle zip is imported to it, the Active Directory STIG items that cannot be applied through GPOs are run (requires the ActiveDirectoryStig module),
            the DNS STIG items are applied (requires the DnsStig module), extracts and config files used for STIG implementation like Java or Mozilla or other config files like custom lockscreen backgrounds to the sysvol,
            creates a standard set of WMI filters for GPOs (requires the ActiveDirectoryTools module), imports the base GPO bundle, links those GPOs to the OUs specified in the GP Links configuration file, imports
            any additional GPOs specified and links those, then finally creates Fine Grained Password policies that comply with the DISA STIG requirements (one is set as the default and another is created with the
            same settings called FGP_STIG and is applied to Domain Users.
             
        .PARAMETER ConfigFilePath
            The path to the configuration file with the parameters used to run this cmdlet. This defaults to "$PSScriptRoot\config.json".
     
        .PARAMETER SiteName
            The new name of the Default First Site.
 
        .PARAMETER SiteSubnets
            The subnets to add to the default site.
 
        .PARAMETERS OUConfigFilePath
            The path to the configuration file for the OU structure to be deployed in the new forest root domain. This defaults to a standard OU structure depending on whether the deployment is for an ESAE environment or not.
 
        .PARAMETER ForestGroupsConfigFilePath
            The path to the configuration file for the security groups to be created that will be utilized to manage the new forest. The defaults to a standard set of groups.
 
        .PARAMETER GPOBundleFilePath
            The path to the zip file containing the base GPOs. This defaults to "$PSScriptRoot\BaseGPOBundle.zip".
 
        .PARAMETER GPLinksFilePath
            The path to the configuration file that maps the imported base GPOs specified in GPOBundleFilePath to OUs. This will default to "$PSScriptRoot\Forest_GPLinks.json" for a standalone forest install and
            default to "$PSScriptRoot\ESAE_GPLinks.json" for an ESAE installation.
 
        .PARAMETER AdditonalGPOsFilePath
            The path to a zip file containing any additional GPOs that should be imported. This is useful so that the base GPO bundle can contain just the STIG GPOs, and this can contain environment specific
            GPOs. If no file is specified, no additional GPOs are imported.
     
        .PARAMETER AdditionalGPLinksFilePath
            The path to the configuration file that maps the imported additional GPOs specified in AdditionalGPOsFilePath to OUs.
 
        .PARAMETER BaseAdmxBundleFilePath
            The path to the zip file with the ADMX and ADML files that will be added to the central policy store. This defaults to "$PSScriptRoot\BaseAdmxBundle.zip". To use the local PolicyDefinitions folder, set this parameter as an empty string.
     
        .PARAMETER AddOnAdmxBundleFilePath
            The path to the zip file with additional ADMX and ADML files. This is useful so that the base bundle can remain unchanged and the add-on can be environment specific.
 
        .PARAMETER IsESAEForest
            Specify that this forest will be an ESAE forest. This option prompts additional available dynamic parameters needed to configure the ESAE forest and forest trust.
 
        .PARAMETER UsersFilePath
            The path to the configuration file that specifies users to be created and their group membership. This defaults to none.
 
        .PARAMETER AdditionalSitesConfigFilePath
            The path to the configuration file that specifies additional AD sites and subnets to be created.
 
        .PARAMETER ManagementGroupsConfigFilePath
            The path to the configuration file that specifies groups to be created that will be used to manage remote forests and domains from the new ESAE forest.
 
        .PARAMETER SysvolFilesPath
            The path to the zip containing files and folders that will be extracted to the SYSVOL into a folder named "files". This defaults to "$PSScriptRoot\Sysvolfiles.zip".
         
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-ADForestSecurityConfiguration -SiteName "Site1" `
                                            -SiteSubnets @("192.168.1.0/24") `
                                            -OUConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_OU.json" `
                                            -UsersFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\users.json" `
                                            -IsESAEForest `
                                            -AdditionalGPOsFilePath "$env:SystemRoot\windowspowershell\v1.0\modules\ESAE\ESAE_AdditionalGPOBundle.zip" `
                                            -AdditionalGPLinksFilePath "$env:SystemRoot\windowspowershell\v1.0\ESAE\modules\ESAE_AdditionalGPLinks.json" `
                                            -ManagementGroupsConfigFilePath "$env:SystemRoot\windowspowershell\v1.0\ESAE\modules\ESAE_RemoteManagementGroups.json" `
 
            Runs the security configuration with the specified parameters. This uses the default for the groups used to manage the ESAE forest.
 
        .NOTES
            None
    #>

    [CmdletBinding(DefaultParameterSetName="File")]
    Param (
        [Parameter(ParameterSetName="File")]
        [ValidateScript({Test-Path -Path $_})]    
        [string]$ConfigFilePath = $script:ConfigFilePath,
        [Parameter(ParameterSetName="Parameters")]
        [string]$SiteName = "Default-First-Site-Name",
        [Parameter(ParameterSetName="Parameters")]
        [string[]]$SiteSubnets = @(),
        [Parameter(ParameterSetName="Parameters")]    
        [string]$OUConfigFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [string]$ForestGroupsConfigFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]        
        [string]$GPOBundleFilePath = $script:BaseGPOBundle,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]        
        [string]$GPLinksFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]    
        [string]$AdditionalGPLinksFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [string]$AdditionalGPOsFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$BaseAdmxBundleFilePath = $script:BaseAdmxBundle,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$AddOnAdmxBundleFilePath = [System.String]::Empty,
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$SysvolFilesPath = $script:SysvolFiles,
        [Parameter(ParameterSetName="Parameters")]
        [switch]$IsESAEForest,
        [Parameter(ParameterSetName="Parameters")]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) {
                Test-Path -Path $_
            }
            else {
                return $true
            }
        })]
        [string]$AdditionalSitesConfigFilePath = [System.String]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [string]$UsersFilePath = [System.String]::Empty
    )
    DynamicParam
    {
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        if ($IsESAEForest) {

            [System.Management.Automation.ParameterAttribute]$Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
            $Attributes.ParameterSetName = "Parameters"
            $Attributes.ValueFromPipelineByPropertyName = $true

            $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($Attributes)

            [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("ManagementGroupsConfigFilePath", [string], $AttributeCollection)
            $ParamDictionary.Add("ManagementGroupsConfigFilePath", $DynParam)
        }

        return $ParamDictionary  
    }

    Begin {

        switch ($PSCmdlet.ParameterSetName) {
            "File" {
                Write-Log "Getting configuration file."
                $Parameters = ConvertFrom-Json -InputObject (Get-Content -Path $ConfigFilePath -Raw)
                Write-Log "Successfully retrieved configuration file."

                $IsESAEForest = $Parameters.IsESAEForest
                $ForestRootDomainName = $Parameters.ForestRootDomainName

                if (![System.String]::IsNullOrEmpty($Parameters.SiteName)) {
                    $SiteName = $Parameters.SiteName
                }
                else {
                    $SiteName = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.AdditionalSitesConfigFilePath)) {
                    $AdditionalSitesConfigFilePath = $Parameters.AdditionalSitesConfigFilePath
                }
                else {
                    $AdditionalSitesConfigFilePath = [System.String]::Empty
                }
                
                if (![System.String]::IsNullOrEmpty($Parameters.GPOBundleFilePath) -and (Test-Path -Path $Parameters.GPOBundleFilePath)) {
                    $GPOBundleFilePath = $Parameters.GPOBundleFilePath
                }
                else {
                    $GPOBundleFilePath = $script:BaseGPOBundle
                }

                if (![System.String]::IsNullOrEmpty($Parameters.GPLinksFilePath) -and (Test-Path -Path $Parameters.GPLinksFilePath)) {
                    $GPLinksFilePath = $Parameters.GPLinksFilePath                
                }
                else {
                    if ($IsESAEForest) {
                        $GPLinksFilePath = $script:ESAEGPLinks
                    }
                    else {
                        $GPLinksFilePath = $script:ForestGPLinks
                    }
                }
                
                if ($Parameters.SiteSubnets -ne $null -and $Parameters.SiteSubnets.Length -gt 0) {
                    $SiteSubnets = $Parameters.SiteSubnets
                }
                else {
                    $SiteSubnets = @()
                }

                if (![System.String]::IsNullOrEmpty($Parameters.SysvolFilesPath) -and (Test-Path -Path $Parameters.SysvolFilesPath)) {
                    $SysvolFilesPath = $Parameters.SysvolFilesPath
                }
                else {
                    if (Test-Path -Path $script:SysvolFiles) {
                        $SyvolFilesPath = $script:SysvolFiles
                    }
                    else {
                        $SysvolFilesPath = [System.String]::Empty
                    }
                }

                if (![System.String]::IsNullOrEmpty($Parameters.UsersFilePath) -and (Test-Path -Path $Parameters.UsersFilePath)) {
                    $UsersFilePath = $Parameters.UsersFilePath

                }
                else {
                    $UsersFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.AdditionalGPOsFilePath)) {
                    $AdditionalGPOsFilePath = $Parameters.AdditionalGPOsFilePath
                }
                else {
                    $AdditionalGPOsFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.AdditionalGPLinksFilePath)) {
                    $AdditionalGPLinksFilePath = $Parameters.AdditionalGPLinksFilePath
                }
                else {
                    $AdditionalGPLinksFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.OUConfigFilePath)) {
                    $OUConfigFilePath = $Parameters.OUConfigFilePath
                }
                else {
                    $OUConfigFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.ForestGroupsConfigFilePath)) {
                    $ForestGroupsConfigFilePath = $Parameters.ForestGroupsConfigFilePath
                }
                else {
                    $ForestGroupsConfigFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.ManagementGroupsConfigFilePath)) {
                    $ManagementGroupsConfigFilePath = $Parameters.ManagementGroupsConfigFilePath
                }
                else {
                    $ManagementGroupsConfigFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.BaseAdmxBundleFilePath) -and (Test-Path -Path $Parameters.BaseAdmxBundleFilePath)) {
                    $BaseAdmxBundleFilePath = $Parameters.BaseAdmxBundleFilePath
                }
                else {
                    $BaseAdmxBundleFilePath = [System.String]::Empty
                }

                if (![System.String]::IsNullOrEmpty($Parameters.AddOnAdmxBundleFilePath) -and (Test-Path -Path $Parameters.AddOnAdmxBundleFilePath)) {
                    $AddOnAdmxBundleFilePath = $Parameters.AddOnAdmxBundleFilePath
                }
                else {
                    $AddOnAdmxBundleFilePath = [System.String]::Empty
                }

                break
            }
            "Parameters" {
                $Counter = 0
                $ForestRootDomainName = ""
                $TimeoutLimit = 10
                while ([System.String]::IsNullOrEmpty($ForestRootDomainName) -and $Counter -lt $TimeoutLimit) {
                    Start-Sleep -Seconds 10
                    try {
                        $ForestRootDomainName = Get-ADForest -Current LocalComputer -Server $env:COMPUTERNAME | Select-Object -ExpandProperty Name
                    }
                    catch [Exception] {
                        Write-Log -Message "Could not call Get-ADForest with error: $($_.Exception.Message)."
                        $Counter++            
                    }

                    if ($Counter -eq $TimeoutLimit) {
                        Write-Log -Message "Timeout waiting to successfully call Get-ADForest, using computer domain."
                        $ForestRootDomainName = Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty Domain
                    }
                }
                break
            }
        }

        if ([System.String]::IsNullOrEmpty($GPOBundleFilePath)) {
            $GPOBundleFilePath = $script:BaseGPOBundle
        }

        if (![System.String]::IsNullOrEmpty($GPLinksFilePath)) {
            if ($IsESAEForest) {
                $GPLinksFilePath = $script:ESAEGPLinks
            }
            else {
                $GPLinksFilePath = $script:ForestGPLinks
            }        
        }

        if (![System.String]::IsNullOrEmpty($ForestGroupsConfigFilePath) -and !(Test-Path -Path $ForestGroupsConfigFilePath)) {
            Write-Warning "The file specified for the forest groups, $ForestGroupsConfigFilePath, could not be found."
            Write-Log "The file specified for the forest groups, $ForestGroupsConfigFilePath, could not be found."
            throw [System.IO.FileNotFoundException]("The file specified for the forest groups, $ForestGroupsConfigFilePath, could not be found.")
        }

        if (![System.String]::IsNullOrEmpty($UsersFilePath) -and !(Test-Path -Path $UsersFilePath)) {
            Write-Warning "The file specified for the users import, $UsersFilePath, could not be found."
            Write-Log "The file specified for the users import, $UsersFilePath, could not be found."
            throw [System.IO.FileNotFoundException]("The file specified for the users import, $UsersFilePath, could not be found.")
        }

        if (![System.String]::IsNullOrEmpty($AdditionalGPOsFilePath) -and !(Test-Path -Path $AdditionalGPOsFilePath)) {
            Write-Warning "The file specified for the additional gpo import, $AdditionalGPOsFilePath, could not be found."
            Write-Log "The file specified for the additional gpo import, $AdditionalGPOsFilePath, could not be found."
            throw [System.IO.FileNotFoundException]("The file specified for the additional gpo import, $AdditionalGPOsFilePath, could not be found.")
        }

        if (![System.String]::IsNullOrEmpty($AdditionalGPLinksFilePath) -and !(Test-Path -Path $AdditionalGPLinksFilePath)) {
            Write-Warning "The file specified for the additional gpo links, $AdditionalGPLinksFilePath, could not be found."
            Write-Log "The file specified for the additional gpo links, $AdditionalGPLinksFilePath, could not be found."
            throw [System.IO.FileNotFoundException]("The file specified for the additional gpo links, $AdditionalGPLinksFilePath, could not be found.")
        }

        if (![System.String]::IsNullOrEmpty($OUConfigFilePath) -and !(Test-Path -Path $OUConfigFilePath)) {
            Write-Warning "The file specified, $OUConfigFilePath, for the OU configuration could not be found."
            Write-Log "The file specified, $OUConfigFilePath, for the OU configuration could not be found."
            throw [System.IO.FileNotFoundException]("The file specified, $OUConfigFilePath, for the OU configuration could not be found.")
        }

        if ($IsESAEForest -and ![System.String]::IsNullOrEmpty($ManagementGroupsConfigFilePath) -and !(Test-Path -Path $ManagementGroupsConfigFilePath)) {
            Write-Warning "The file specified for the managemement groups, $ManagementGroupsConfigFilePath, could not be found."
            Write-Log "The file specified for the management groups, $ManagementGroupsConfigFilePath, could not be found."
            throw [System.IO.FileNotFoundException]("The file specified for the management groups, $ManagementGroupsConfigFilePath, could not be found.")
        }
        
        [bool]$Success = $true

        $Counter = 0
        $NTDS = Get-Service -Name NTDS

        while ($NTDS.Status -ne "Running") {
            Start-Sleep -Seconds 1
            $Counter++
            $NTDS = Get-Service -Name NTDS

            if ($Counter -gt 600) {
                throw "Timeout waiting for Active Directory Domain Services to start."
            }
        }

        $Counter = 0
        $ADWS = Get-Service -Name ADWS

        while ($ADWS.Status -ne "Running") {
            Start-Sleep -Seconds 1
            $Counter++
            $ADWS = Get-Service -Name ADWS

            if ($Counter -gt 600) {
                throw "Timeout waiting for Active Directory Web Services to start."
            }
        }

        $DomainSID = Get-ADDomain -Identity $ForestRootDomainName -Server $env:COMPUTERNAME | Select-Object -ExpandProperty DomainSID
        $DomainUsersSID = (New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::AccountDomainUsersSid, $DomainSID)).Value.ToString()
        
        $SYSVOL = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" | Select-Object -ExpandProperty SysVol

    }

    Process 
    {
        #region AD Sites
        if (![System.String]::IsNullOrEmpty($SiteName)) {
            try 
            {
                Write-Log "Renaming site."
                Rename-ADSite -NewName $SiteName
            }
            catch [Exception] 
            {
                Write-Log $_
                $Success = $false
            }
        }

        if ($SiteSubnets -ne $null -and $SiteSubnets.Count -gt 0) {    
            foreach ($Subnet in $SiteSubnets) {
                try
                {
                    Write-Log "Adding subnet $Subnet."
                    Add-ADSiteSubnet -SiteSubnet $Subnet -SiteName $SiteName
                }
                catch [Exception] 
                {
                    Write-Log $_
                    $Success = $false
                }
            
            }            
        }

        if (![System.String]::IsNullOrEmpty($AdditionalSitesConfigFilePath)) {
            Write-Log "Additional site information provided, converting from Json to object."
            try {
                $AdditionalSitesJson = Get-Content -Path $AdditionalSitesConfigFilePath -Raw
                $AdditionalSites = ConvertFrom-Json -InputObject $AdditionalSitesJson

                foreach ($Site in $AdditionalSites) {
                    Write-Log "Adding site $($Site.Name)."

                     try{
                        $NewSite = Get-ADReplicationSite -Identity $Site.Name
                         Write-Log "Site $($NewSite.Name) already exists, continuing."
                    }
                    catch [Exception] {
                        try {
                            $NewSite = New-ADReplicationSite -Name $Site.Name -PassThru -ProtectedFromAccidentalDeletion $true
                            Write-Log "Successfully created new site."
                        }
                        catch [Exception] {
                            Write-Log $_
                        }
                    }

                    if ($NewSite -ne $null) {
                        foreach ($Subnet in $Site.Subnets) {
                            Write-Log "Adding $Subnet to site $($NewSite.Name)."
                            Add-ADSiteSubnet -SiteName $NewSite.Name -SiteSubnet $Subnet
                        }
                    }
                    else {
                        Write-Log "New site object was null, not adding subnets."
                    }
                }
            }
            catch [Exception] {
                Write-Log $_
            }
        }

        #endregion

        try {
            Write-Log "Installing KDS Root Key, effective immediately."
            Add-KdsRootKey -EffectiveImmediately
            Write-Log "Installed KDS Root Key."
        }
        catch [Exception] {
            Write-Log $_
        }

        try 
        {
            Write-Log "Enabling Recycle Bin."
            Enable-ADRecycleBin -ForestRootDomainName $ForestRootDomainName
        }
        catch [Exception] 
        {
            Write-Log $_
        }

        try
        {
            Write-Log "Adding sceregvl.inf to add MSS settings."
            New-MSSGPOSettings -Wait
            Write-Log "Completed adding sceregvl.inf file."
        }
        catch [Exception]
        {
            Write-Log $_
        }
        
        #region OU and Group Structure

        try
        {
            Write-Log "Creating Forest OU Structure."
            if ([System.String]::IsNullOrEmpty($OUConfigFilePath)) {
                if ($IsESAEForest) {
                    Write-Log "Config file was null or empty, but this is an ESAE forest installation, using the ESAE_OU.json file for the configuration."
                    Set-OUStructure -FilePath $script:ESAEOUConfigFilePath
                }
                else {
                    Set-OUStructure -OUStructureJson ([System.String]::Empty)
                }
            }
            else {
                Set-OUStructure -FilePath $OUConfigFilePath
            }

            Write-Log "Completed creating OU structure."

            try
            {
                Write-Log "Creating local forest management groups."

                if ([System.String]::IsNullOrEmpty($ForestGroupsConfigFilePath)) {
                    if ($IsESAEForest) {
                        Write-Log -Message "Creating groups for an ESAE forest."
                        $GroupPrefix = "UG-ESAE"
                        $ResourceOUName = "ESAE Privileged Resources"
                    }
                    else {
                        Write-Log -Message "Creating groups for a standalone forest."
                        $GroupPrefix = "UG"
                        $ResourceOUName = "Privileged Resources"
                    }

                    New-StandardForestGroups -GroupNamePrefix $GroupPrefix -ResourceOUName $ResourceOUName
                }
                else {
                    Write-Log -Message "Using configuration file to create groups: $ForestGroupsConfigFilePath."
                    New-StandardForestGroups -FilePath $ForestGroupsConfigFilePath
                }
        
                Write-Log "Completed creating local forest management groups."
            }
            catch [Exception] 
            {
                Write-Log $_
                $Success = $false
            }

            if ($IsESAEForest) {
                try
                {
                    Write-Log "Creating remote forest management groups."
            
                    if ([System.String]::IsNullOrEmpty($ManagementGroupsConfigFilePath)) {
                        if ([System.String]::IsNullOrEmpty($OUConfigFilePath)) {
                            New-ManagementGroups
                        }
                        else {
                            Write-Log "No management group config file path specified, but a custom OU structure was specified. Cannot create default groups with a custom OU structure."
                        }
                    }
                    else {
                        New-ManagementGroups -FilePath $ManagementGroupsConfigFilePath
                    }

                    Write-Log "Completed creating remote forest management groups."
                }
                catch [Exception] 
                {
                    Write-Log $_
                    $Success = $false
                }
            }
        }
        catch [Exception] 
        {
            Write-Log $_
            $Success = $false
        }

        #endregion

        try 
        {
            Write-Log "Building central policy store."
            New-CentralPolicyStore -FilePath $BaseAdmxBundleFilePath -SysVolPath $SYSVOL -ForestRootDomainName $ForestRootDomainName

            if (![System.String]::IsNullOrEmpty($AddOnAdmxBundleFilePath)) {
                try
                {
                    Write-Log "Importing Add-On ADMX files."
                    Add-AdmxBundleToCentralPolicyStore -FilePath $AddOnAdmxBundleFilePath -SysVolPath $SYSVOL -ForestRootDomainName $ForestRootDomainName
                }
                catch [Exception] 
                {
                    Write-Log $_
                    $Success = $false
                }
            }
        }
        catch [Exception] 
        {
            Write-Log $_
            $Success = $false
        }

        try {
            Write-Log "Setting Active Directory non GPO STIG items."
            Import-Module ActiveDirectoryStig
            Set-ActiveDirectoryStigItems
            Write-Log "Completed Active Directory STIG."
        }
        catch [Exception] {
            Write-Log $_
            $Success = $false
        }

        try {
            Write-Log "Setting DNS Server STIG."
            Import-Module DnsStig
            Set-DnsServerStig -Forest $ForestRootDomainName
            Write-Log "Completed DNS Server STIG."
        }
        catch [Exception] {
            Write-Log $_
            $Success = $false
        }

        #region GPO Operations

        if (![System.String]::IsNullOrEmpty($SysvolFilesPath)) {
            try {
                Write-Log "Extracting config files for GPO use to the SYSVOL."
                Add-SysvolFiles
                Write-Log "Finished extracting config files."
            }
            catch [Exception] {
                Write-Log $_
                $Success = $false
            }
        }
        else {
            Write-Log -Message "No Sysvol files zip specified."
        }

        try
        {
            Write-Log "Creating WMI Filters."
            Import-Module ActiveDirectoryTools
            New-StandardGPOWmiFilters
            Write-Log "Finished creating standard WMI filters."
        }
        catch [Exception] {
            Write-Log $_
            $Success = $false
        }

        try
        {
            Write-Log "Importing baseline GPOs."
            $Destination = [System.IO.Path]::Combine($PSScriptRoot,"GPOs")
            Write-Log "Extracting GPO bundle."
            Extract-ZipFile -Source $GPOBundleFilePath -Destination $Destination
            Write-Log "Creating Migration Table."
            $MigrationTablePath = New-GPOMigrationTable -BackupRootDirectory $Destination
            Write-Log "Setting preference values."
            Set-GPPMigrationValues -BackupRootDirectory $Destination
            Write-Log "Importing GPOs to domain from $Destination for domain $ForestRootDomainName using migration table at $MigrationTablePath."
            Import-FullGPOBackups -Path $Destination -Domain $ForestRootDomainName -MigrationTable $MigrationTablePath
            Write-Log "Deleting temp GPO folder."
            Remove-Item -Path $Destination -Force -Recurse -Confirm:$false
            Remove-Item -Path $MigrationTablePath -Force -Recurse -Confirm:$false
            Write-Log "Completed importing GPOs."

            try {
                Write-Log "Linking GPOs."
                New-GPOLinks -LinksFilePath $GPLinksFilePath
                try {
                    Write-Log "Removing default domain controllers policy."
                    $TopLevelDomain = Get-ADDomain -Identity $ForestRootDomainName | Select-Object -ExpandProperty DistinguishedName
                    Remove-GPLink -Name "Default Domain Controllers Policy" -Target "OU=Domain Controllers,$TopLevelDomain" -ErrorAction Stop
                    Write-Log "Successfully removed default domain controllers policy."
                }
                catch [Exception] {
                    Write-Log $_
                }
                Write-Log "Completed linking GPOs."
            }
            catch [Exception] {
                Write-Log $_
                $Success = $false
            }
        }
        catch [Exception] {
            Write-Log $_
            $Success = $false
        }

        if (![System.String]::IsNullOrEmpty($AdditionalGPOsFilePath)) {
            try {
                Write-Log "Importing additional GPOs."
                $Destination = [System.IO.Path]::Combine($PSScriptRoot,"AdditionalGPOs")
                Extract-ZipFile -Source $AdditionalGPOsFilePath -Destination $Destination
                Write-Log "Creating Migration Table"
                $MigrationTablePath = New-GPOMigrationTable -BackupRootDirectory $Destination
                Write-Log "Setting preference values."
                Set-GPPMigrationValues -BackupRootDirectory $Destination
                Write-Log "Importing GPOs to domain from $Destination for domain $ForestRootDomainName using migration table at $MigrationTablePath."
                Import-FullGPOBackups -Path $Destination -Domain $ForestRootDomainName -MigrationTable $MigrationTablePath
                Write-Log "Deleting temp GPO folder."
                Remove-Item -Path $Destination -Force -Recurse -Confirm:$false
                Remove-Item -Path $MigrationTablePath -Force -Recurse -Confirm:$false
                Write-Log "Completed importing GPOs."

                if (![System.String]::IsNullOrEmpty($AdditionalGPLinksFilePath) -and (Test-Path -Path $AdditionalGPLinksFilePath)) {
                    try {
                        Write-Log "Linking Additional GPOs."
                        New-GPOLinks -LinksFilePath $AdditionalGPLinksFilePath
                        Write-Log "Completed linking additional GPOs."
                    }
                    catch [Exception] {
                        Write-Log $_
                        $Success = $false
                    }
                }
            }
            catch [Exception] {
                Write-Log $_
                $Success = $false
            }
        }

        #endregion

        try {
            Write-Log "Creating fine grained password policies."
            Write-Log "Setting default domain password policy settings objects."

            Set-ADDefaultDomainPasswordPolicy   -Identity $ForestRootDomainName `
                                                -ComplexityEnabled $true `
                                                -LockoutDuration ([System.Timespan]::MinValue) `
                                                -LockoutObservationWindow ([System.Timespan]::FromMinutes(60)) `
                                                -LockoutThreshold 3 `
                                                -MinPasswordLength 14 `
                                                -MaxPasswordAge ([System.Timespan]::FromDays(60)) `
                                                -MinPasswordAge ([System.Timespan]::FromDays(1)) `
                                                -ReversibleEncryptionEnabled $false `
                                                -PasswordHistoryCount 24 `
                                                -Server $env:COMPUTERNAME `
            
            Write-Log "Creating STIG password settings object."
            
            $PSO = New-ADFineGrainedPasswordPolicy  -Name "FGP_STIG" `
                                                    -ComplexityEnabled $true `
                                                    -DisplayName "FGP_STIG" `
                                                    -LockoutDuration ([System.Timespan]::MinValue) `
                                                    -LockoutObservationWindow ([System.Timespan]::FromMinutes(60)) `
                                                    -LockoutThreshold 3 `
                                                    -MinPasswordLength 14 `
                                                    -MaxPasswordAge ([System.Timespan]::FromDays(60)) `
                                                    -MinPasswordAge ([System.Timespan]::FromDays(1)) `
                                                    -ProtectedFromAccidentalDeletion $true `
                                                    -ReversibleEncryptionEnabled $false `
                                                    -PasswordHistoryCount 24 `
                                                    -Precedence ([System.Int32]::MaxValue) `
                                                    -Server $env:COMPUTERNAME `
                                                    -PassThru

            Add-ADFineGrainedPasswordPolicySubject -Identity $PSO -Subjects $DomainUsersSID -Server $env:COMPUTERNAME

            Write-Log "Created default and domain Password Settings Objects."
        }
        catch [Exception] {
            Write-Log $_
            $Success = $false
        }

        if (![System.String]::IsNullOrEmpty($UsersFilePath)) {
            try {
            
                Write-Log "Importing users."
                Import-ADUsers -FilePath $UsersFilePath -EnableLogging
                Write-Log "Completed user import."
            }
            catch [Exception] {
                Write-Log $_
                $Success = $false
            }
        }
    }

    End {
        Write-Log "Checking for Finish AD Forest Scheduled Task."

        if ((Get-ScheduledTask -TaskName $script:FinishSecureADForestTask -ErrorAction SilentlyContinue) -ne $null) {
            Write-Log "Removing Finish AD Forest Scheduled Task."
            Unregister-ScheduledTask -TaskName $script:FinishSecureADForestTask -Confirm:$false
        }

        if ($Success) {
            Write-Log "Completed forest install."

            if ($IsESAEForest) {
                Write-Log "Starting ESAE Configuration function."
                Set-ESAEForestConfiguration -ConfigFilePath $ConfigFilePath
            }
        }
        else {
            Write-Log "One of the steps in Set-ADForestSecurityConfiguration failed."
            $CommandText = Get-CommandText -Command ($PSCmdlet.MyInvocation.MyCommand.Name) -Parameters $PSBoundParameters
            Write-Log "Rerun this command: $CommandText"
        }
    }
}

Function Set-ESAEForestConfiguration {
    <#
        .SYNOPSIS
            This cmdlet conducts the steps to set up the forest trust and DNS settings to enable the ESAE management forest.
 
        .DESCRIPTION
            The cmdlet builds the conditional forwarders in the two forests, if specified, and then sets up the one-way forest trust from the remote forest to the new ESAE forest.
 
        .PARAMETER ConfigFilePath
            The path to the configuration file with the parameters used to run this cmdlet. This defaults to "$PSScriptRoot\config.json".
 
        .PARAMETER RemoteForestCredential
            The credentials to use to connect to the remote forest during an ESAE installation to create the conditional forwarder pointing to the ESAE forest and setup the forest trust.
 
        .PARAMETER RemoteForest
            The DNS name of the remote forest during an ESAE installation.
 
        .PARAMETER LocalForest
            The DNS name of the local forest being setup for ESAE. This is only required if the forest being setup is different than the local forest the cmdlet is running on. Typically this is not required.
 
        .PARAMETER LocalForestCredential
            Specifies the credentials to use on the local forest to set up the forest trust for an ESAE forest. This is only needed if the specific credentials are needed in the local forest, typically not required.
 
        .PARAMETER CreateRemoteConditionalForwarder
            Specify that a conditional forwarder to the new ESAE forest should be created in the remote forest. Requires RemoteForestCredential is specified.
 
        .PARAMETER CreateLocalConditionalForwarder
            Specify that a conditional forwarder to the remote forest should be created in the new ESAE forest. This needs to be specified unless an external DNS solution is utilized to resolve the remote forest.
 
        .PARAMETER RemoteForestMasterServers
            The remote DNS servers that will be used to create the local conditional forwarder in the new ESAE forest.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-ESAEForestConfiguration -RemoteForest "contoso.com" `
                                -RemoteForestMasterServers @("192.168.2.1") `
                                -CreateLocalConditionalForwarder `
                                -RemoteForestCredential (Get-Credential) `
                                -CreateRemoteConditionalForwarder `
 
        .NOTES
            The remote forest credential should have Enterprise Admin rights to build the conditional forwarder and establish the forest trust.
    #>

    [CmdletBinding(DefaultParameterSetName="File")]
    Param (
        [Parameter(ParameterSetName="File")]
        [ValidateScript({Test-Path -Path $_})]    
        [string]$ConfigFilePath = $script:ConfigFilePath,
        [Parameter(ParameterSetName="Parameters",Mandatory=$true)]
        [string]$RemoteForest,
        [Parameter(ParameterSetName="Parameters",Mandatory=$true)]
        [PSCredential]$RemoteForestCredential,
        [Parameter(ParameterSetName="Parameters")]
        [PSCredential]$LocalForestCredential = [PSCredential]::Empty,
        [Parameter(ParameterSetName="Parameters")]
        [switch]$CreateLocalConditionalForwarder,
        [Parameter(ParameterSetName="Parameters")]
        [switch]$CreateRemoteConditionalForwarder,
        [Parameter(ParameterSetName="Parameters")]
        [string[]]$RemoteForestMasterServers = @()
    )

    Begin {
        Write-Log "Starting ESAE Forest Configuration."

        switch ($PSCmdlet.ParameterSetName) {
            "File" {
                Write-Log "Getting config file from $ConfigFilePath."
                $Parameters = ConvertFrom-Json -InputObject (Get-Content -Path $ConfigFilePath -Raw)

                Write-Log "Remote forest password file $($Parameters.RemoteForestPasswordFilePath)."

                $RemoteForest = $Parameters.RemoteForest
                $RemoteForestCredentialPassword = Get-EncryptedPassword -FilePath $Parameters.RemoteForestPasswordFilePath
                $RemoteForestCredential = New-Object -TypeName System.Management.Automation.PSCredential($Parameters.RemoteForestUserName, $RemoteForestCredentialPassword)

                try {
                    Write-Log "Removing remote forest password file."
                    Remove-Item -Path $Parameters.RemoteForestPasswordFilePath
                    Write-Log "Removed password file."
                }
                catch [Exception] {
                    Write-Log $_
                }

                try {
                    if (![System.String]::IsNullOrEmpty($Parameters.LocalForestPasswordFilePath) -and (Test-Path -Path $Parameters.LocalForestPasswordFilePath) -and ![System.String]::IsNullOrEmpty($Parameters.LocalForestUserName)) {
                        Write-Log "Local forest credentials specified."
                        $LocalForestCredentialPassword = Get-EncryptedPassword -FilePath $Parameters.LocalForestPasswordFilePath
                        $LocalForestCredential = New-Object -TypeName System.Management.Automation.PSCredential($Parameters.LocalForestUserName, $LocalForestCredentialPassword)

                        Write-Log "Removing local forest password file."
                        Remove-Item -Path $Parameters.RemoteForestPasswordFilePath
                        Write-Log "Removed password file."

                    } else {
                        $LocalForestCredential = [PSCredential]::Empty
                    }
                }
                catch [Exception] {
                    Write-Log $_
                }

                if ($Parameters.CreateLocalConditionalForwarder -ne $null) {
                    $CreateLocalConditionalForwarder = $Parameters.CreateLocalConditionalForwarder
                }
                else {
                    $CreateLocalConditionalForwarder = $false
                }
                
                if ($CreateLocalConditionalForwarder) {
                    if ($Parameters.RemoteForestMasterServers -ne $null -and $Parameters.RemoteForestMasterServers.Count -gt 0) {
                        $RemoteForestMasterServers = $Parameters.RemoteForestMasterServers
                    }
                    else {
                        Write-Log "Create local stub zone was specified, but the remote forest master servers array was null or empty."
                        throw "Create local stub zone was specified, but the remote forest master servers array was null or empty."
                    }
                }

                if ($Parameters.CreateRemoteConditionalForwarder -ne $null) {
                    $CreateRemoteConditionalForwarder = $Parameters.CreateRemoteConditionalForwarder
                }
                else {
                    $CreateRemoteConditionalForwarder = $false
                }
            
                break
            }
            "Parameters" {

                break
            }
        }

        [bool]$Success = $false
        $ForestRootDomainName = Get-ADForest -Current LocalComputer -Server $env:COMPUTERNAME | Select-Object -ExpandProperty Name
    }

    Process {
        
        try {
            Write-Log "Creating forest trust with $RemoteForest."

            New-ADForestTrust -RemoteForest $RemoteForest `
                                -RemoteForestCredential $RemoteForestCredential `
                                -LocalForestCredential $LocalForestCredential `
                                -CreateLocalConditionalForwarder:$CreateLocalConditionalForwarder `
                                -CreateRemoteConditionalForwarder:$CreateRemoteConditionalForwarder `
                                -RemoteForestMasterServers $RemoteForestMasterServers `
                                -EnableSelectiveAuthentication $true `
                                -SidFilteringEnabled $true `
                                -TrustDirection Inbound `
                                -EnableLogging `
                                -TrustingDomainSupportsKerberosAESEncryption $true `
                                -WaitForRpcSs
        }
        catch [Exception] {
            Write-Log $_
        }
    }

    End {
        Write-Log "Completed ESAE Forest Configuration."
    }
}

#endregion

#region AD Forest Prerequisites

Function Add-ADDSWindowsFeature {
    <#
        .SYNOPSIS
            Installs the Active Directory feature.
 
        .DESCRIPTION
            Installs the Active Directory feature and optionally installs the management tools.
 
        .PARAMETER IncludeManagementTools
            Specify whether the management tools should be installed, this defaults to false.
 
        .INPUTS
            System.Boolean
         
        .OUTPUTS
            Microsoft.Windows.ServerManager.Commands.FeatureOperationResult
 
        .EXAMPLE
            Add-ADDSWindowsFeature -IncludeManagementTools
             
            Installs the Active Directory feature with the management tools.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true)]
        [switch]$IncludeManagementTools = $false
    )
    Begin {}

    Process {
        $Feature = Add-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools:$IncludeManagementTools
    }

    End {
        Write-Output $Feature
    }
}

Function Set-LocalAdminPassword {
    <#
        .SYNOPSIS
            Sets the local administrator password.
 
        .DESCRIPTION
            Sets the local administrator password and optionally enables the account if it is disabled.
 
        .PARAMETER AdminPassword
            The new password for the local administrator account.
 
        .PARAMETER EnableAccount
            Specify to enable the local administrator account if it is disabled.
 
        .INPUTS
            System.Boolean
         
        .OUTPUTS
            Microsoft.Windows.ServerManager.Commands.FeatureOperationResult
 
        .EXAMPLE
            Set-LocalAdminPassword -EnableAccount
 
            The cmdlet will prompt the user to enter the new password.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [SecureString]$AdminPassword,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableAccount
    )
    Begin {
        $HostName = $env:COMPUTERNAME 
        $Computer = [ADSI]"WinNT://$HostName,Computer" 

        while($AdminPassword -eq $null) {
            $AdminPassword = Read-Host -AsSecureString -Prompt "Enter the new administrator password"
        }
    }
    
    Process {
        foreach ($Child in $Computer.Children | Where {$_.Class -eq "User"} ) { 

            $Sid = New-Object System.Security.Principal.SecurityIdentifier($Child.objectSid[0],0) 
    
            if ($Sid.Value -match "S-1-5-21-.*-500") {
    
                $User = [ADSI]"WinNT://$HostName/$($Child.Name),User"
                $Password = Convert-SecureStringToString -SecureString $AdminPassword
                
                $User.SetPassword($Password)
                
                if ($EnableAccount) {
                    #The 0x0002 flag specifies that the account is diabled
                    #The binary AND operator will test the value to see if the bit is set, if it is, the account is disabled.
                    #Doing a binary OR would add the value to the flags, since it would not be present, the OR would add it
                    if ($User.UserFlags.Value -band "0x0002") {
                        #The binary XOR will remove the flag, which enables the account, the XOR means that if both values have the bit set, the result does not
                        #If only 1 value has the bit set, then it will remain set, so we need to ensure that the bit is actually set with the -band above for the XOR to actually
                        #remove the disabled value
                        $User.UserFlags = $User.UserFlags -bxor "0x0002"
                        $User.SetInfo()
                    }
                }

                break
            }
        }
    }
    
    End {
        return $true
    }       
}

Function Rename-LocalComputer {
    <#
        .SYNOPSIS
            Renames the computer.
 
        .DESCRIPTION
            Renames the computer.
 
        .PARAMETER NewName
            The new name for the computer, must be 15 characters or less.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Rename-LocalComputer -ComputerName "AdminDC"
 
            Renames the local computer to AdminDC.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({$_.Length -le 15})]
        [string]$ComputerName
    )
    Begin {}

    Process {
        $NewName = Rename-Computer -NewName $ComputerName -PassThru
    }

    End {
        Write-Output $NewName
    }
}

#endregion

#region Finish AD Forest Installation

Function Rename-ADSite {
    <#
        .SYNOPSIS
            Renames the specified Active Directory Site.
 
        .DESCRIPTION
            Renames the specified Active Directory Site
 
        .PARAMETER Name
            The current identity of the Active Directory site, this defaults to "Default-First-Site-Name".
 
        .PARAMETER NewName
            The new name of the Active Directory site.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADReplicationSite
 
        .EXAMPLE
            Rename-ADSite -NewName "Headquarters"
 
            Renames the Default-First-Site-Name site to Headquarters.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$Name = "Default-First-Site-Name",
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$NewName
    )

    Begin {}

    Process {
        $Site = Get-ADReplicationSite -Server $env:COMPUTERNAME -Identity $Name | Rename-ADObject -NewName $NewName -Server $env:COMPUTERNAME -PassThru
    }

    End {
        Write-Output $Site
    }
}

Function Add-ADSiteSubnet {
    <#
        .SYNOPSIS
            Adds a single subnet to an Active Directory site.
 
        .DESCRIPTION
            Adds a single subnet to an Active Directory site. If the subnet already exists as a replication subnet, it is assigned to the specified Site, which will remove it from any pre-existing association. If it does not exist, it is added and assigned to the site.
 
        .PARAMETER SiteSubnet
            The subnet to add to the site, should be in the form of X.X.X.X/CIDR.
 
        .PARAMETER SiteName
            The site the subnet will be added to. This defaults to the current site of the computer the cmdlet is being run on.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADReplicationSubnet
 
        .EXAMPLE
            Add-ADSiteSubnet -SiteSubnet "192.168.1.0/24" -SiteName "Headquarters"
 
            The 192.168.1.0/24 subnet is added to the Headquarters site.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$SiteSubnet,
        [Parameter(Position=1,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$SiteName = [System.String]::Empty
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($SiteName)) {
            $SiteName = Get-ADReplicationSite -Server $env:COMPUTERNAME  | Select-Object -First 1 -ExpandProperty Name
        }
    }

    Process {
        try {
            $ExistingSubnet = Get-ADReplicationSubnet -Identity $SiteSubnet -ErrorAction SilentlyContinue

            if ($ExistingSubnet -eq $null) {
                $Subnet = New-ADReplicationSubnet -Name $SiteSubnet -Site $SiteName
            }
            else {
                $Subnet = Set-ADReplicationSubnet -Identity $ExistingSubnet -Site $SiteName -PassThru
            }
        }
        catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
            $Subnet = New-ADReplicationSubnet -Name $SiteSubnet -Site $SiteName
        }
    }

    End {
        Write-Output $Subnet
    }
}

Function Enable-ADRecycleBin {
    <#
        .SYNOPSIS
            Enables the AD recycle bin.
 
        .DESCRIPTION
            Enables the AD recycle bin in the specified forest.
 
        .PARAMETER ForestRootDomainName
            The name of the forest root domain. This defaults to the forest of the computer the cmdlet is being run on.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Enable-ADRecycleBin -ForestRootDomainName "admin.local"
 
            Enables the AD recycle bin in the admin.local forest.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$ForestRootDomainName = [System.String]::Empty
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($ForestRootDomainName)) {
            $ForestRootDomainName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name
        }
    }

    Process {
        Enable-ADOptionalFeature -Identity "Recycle Bin Feature" -Scope ForestOrConfigurationSet -Target $ForestRootDomainName -Confirm:$false
    }

    End {
        Write-Host "AD Recycle Bin successfully enabled" -ForegroundColor Green
    }
}

Function New-CentralPolicyStore {
    <#
        .SYNOPSIS
            Creates the Central Policy Store for GPOs.
 
        .DESCRIPTION
            Creates the Central Policy Store in the SYSVOL. Then, the cmdlet extracts the ADMX and English ADML files in the specified zip, or uses the local ADMX and English ADML files in the PolicyDefinitions folder, and
            moves those to the Central Policy Store. The default included zip file contains the ADMX definitions from Windows 10 1511.
 
        .PARAMETER FilePath
            The path to the zip containing the ADMX and ADML files. This defaults to "$PSScriptRoot\BaseAdmxBundle.zip". Set this option to an empty string to use the local PolicyDefinitions folder instead.
 
        .PARAMETER SysVolPath
            The location of the SYSVOL. This parameter defaults to finding the SYSVOL from "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters\" SysVol property. This property only exists on domain controllers, so if the
            cmdlet is being run remotely, the path should be explicitly specified.
 
        .PARAMETER ForestRootDomainName
            The name of the forest root domain to be used with the SYSVOL path. This defaults to the current forest of the local computer.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            New-CentralPolicyStore
 
            Creates the Central Policy Store using the default ADMX zip bundle for the current forest of the local domain controller the cmdlet is run on.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$FilePath = "$PSScriptRoot\BaseAdmxBundle.zip",
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]        
        [string]$SysVolPath = [System.String]::Empty,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [string]$ForestRootDomainName = [System.String]::Empty
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($SysVolPath)) {
            $SYSVOL = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" | Select-Object -ExpandProperty SysVol
        }
        else {
            $SYSVOL = $SysVolPath
        }

        if ([System.String]::IsNullOrEmpty($ForestRootDomainName)) {
            $ForestRootDomainName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name
        }
    }

    Process {
       
        if ($FilePath -eq $null -or $FilePath -eq [System.String]::Empty -or !(Test-Path -Path $FilePath))  {
            $SourceAdmx = "$env:SYSTEMROOT\PolicyDefinitions\*.admx"
            $SourceAdml = "$env:SYSTEMROOT\PolicyDefinitions\en-us\*.adml"

            $CentralStoreAdmx = "$SYSVOL\$ForestRootDomainName\Policies\PolicyDefinitions"
            $CentralStoreAdml = "$SYSVOL\$ForestRootDomainName\Policies\PolicyDefinitions\en-us"

            New-Item -ItemType Directory -Path $CentralStoreAdmx
            New-Item -ItemType Directory -Path $CentralStoreAdml

            Copy-Item -Path $SourceAdmx -Destination $CentralStoreAdmx
            Copy-Item -Path $SourceAdml -Destination $CentralStoreAdml
        }
        else {
            Extract-ZipFile -Source $FilePath -Destination "$SYSVOL\$ForestRootDomainName\Policies\PolicyDefinitions"
        }
    }

    End {
        Write-Host "Central Policy Store successfullly created." -ForegroundColor Green
    }
}

Function Add-AdmxBundleToCentralPolicyStore {
    <#
        .SYNOPSIS
            Adds the contents of zip file to the Central Policy Store
 
        .DESCRIPTION
            Adds the contents of a zip file to the Central Policy Store.
 
        .PARAMETER FilePath
            The path to the zip containing the ADMX and ADML files.
 
        .PARAMETER SysVolPath
            The location of the SYSVOL. This parameter defaults to finding the SYSVOL from "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters\" SysVol property. This property only exists on domain controllers, so if the
            cmdlet is being run remotely, the path should be explicitly specified.
 
        .PARAMETER ForestRootDomainName
            The name of the forest root domain to be used with the SYSVOL path. This defaults to the current forest of the local computer.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Add-AdmxBundletoCentralPolicyStore -FilePath "$PSScriptRoot\AddOnAdmxBundle.zip"
 
            Adds the contents of the zip file to the central policy store.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$FilePath,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]        
        [string]$SysVolPath = [System.String]::Empty,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [string]$ForestRootDomainName = [System.String]::Empty
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($FilePath) -or !(Test-Path -Path $FilePath))  {
            throw [System.IO.FileNotFoundException]("The source zip file could not be found.")
        }

        if ([System.String]::IsNullOrEmpty($SysVolPath)) {
            $SYSVOL = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" | Select-Object -ExpandProperty SysVol
        }
        else {
            $SYSVOL = $SysVolPath
        }

        if ([System.String]::IsNullOrEmpty($ForestRootDomainName)) {
            $ForestRootDomainName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name
        }
    }

    Process {
        if (Test-Path -Path "$SYSVOL\$ForestRootDomainName\Policies\PolicyDefinitions") {
            Extract-ZipFile -Source $FilePath -Destination "$SYSVOL\$ForestRootDomainName\Policies\PolicyDefinitions"
            Write-Host "Add-on ADMX files successfullly extracted." -ForegroundColor Green
        }
        else {
            Write-Warning -Message "The Policy Definitions folder in the Central Policy Store could not be found."
        }
    }

    End {
        
    }
}

Function New-MSSGPOSettings {
    <#
        .SYNOPSIS
            Adds the new sceregvl.inf file to enable the MSS settings in Group Policy Management.
 
        .DESCRIPTION
            Adds the new sceregvl.inf file to enable the MSS settings in Group Policy Management. The original file is renamed to .old.
 
        .PARAMETER FilePath
            The path to the new sceregvl.inf file. This defaults to "$PSScriptRoot\sceregvl.inf" and shouldn't need to be changed.
 
        .PARAMETER Wait
            The function executes a scheduled task that runs as SYSTEM. This parameter waits for the schedule task to complete before completing.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            New-MSSGPOSettings
 
            Adds the new sceregvl.inf file and enables the management of MSS settings in GPMC.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$FilePath = $script:MSSFilePath,
        [Parameter(Position=1)]
        [switch]$Wait
    )

    Begin{
        $TargetFilePath = "$env:SYSTEMROOT\inf\sceregvl.inf"
        $NewName = "$env:SYSTEMROOT\inf\sceregvl.old"

        $System = New-Object Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::LocalSystemSid, $null)

        $SystemAce = New-Object System.Security.AccessControl.FileSystemAccessRule($System,
            [System.Security.AccessControl.FileSystemRights]::FullControl,
            @([System.Security.AccessControl.InheritanceFlags]::None),
            [System.Security.AccessControl.PropagationFlags]::None,
            [System.Security.AccessControl.AccessControlType]::Allow       
        )
    }

    Process{
        <#$Process = Start-Process -FilePath "$env:SYSTEMROOT\System32\takeown.exe" -ArgumentList @("/f $TargetFilePath") -NoNewWindow
 
        #$Acl = Get-Acl -Path $TargetFilePath
        #$Acl.AddAccessRule($SystemAce)
        #Set-Acl -Path $TargetFilePath -AclObject $Acl
 
        $Process = Start-Process -FilePath "$env:SYSTEMROOT\System32\icacls.exe" -ArgumentList @("$TargetFilePath", "/grant SYSTEM:f")
 
        Rename-Item -Path $TargetFilePath -NewName $NewName
        Copy-Item -Path $FilePath -Destination $TargetFilePath -Force
 
        $Process = Start-Process -FilePath "$env:SYSTEMROOT\System32\regsvr32.exe" -ArgumentList @("scecli.dll") -NoNewWindow#>


        $TaskName = "takeown"

        $Command = @"
        try {
            Import-Module -Name ESAE
            [System.Diagnostics.Process]`$Process = New-Object System.Diagnostics.Process
            `$Process.StartInfo.Filename = "`$env:SYSTEMROOT\System32\takeown.exe"
            `$Process.StartInfo.Arguments = "/f ```"$TargetFilePath```""
            `$Process.StartInfo.RedirectStandardOutput = `$true
            `$Process.StartInfo.UseShellExecute = `$false
            `$Process.StartInfo.CreateNoWindow = `$true
            `$Process.StartInfo.RedirectStandardError = `$true
            `$Process.Start() | Out-Null
            `$Process.WaitForExit()
 
            if (`$Process.ExitCode -ne 0) {
                Write-Log -Message "takeown.exe exited with code `$Process.ExitCode."
                [System.String]`$Out = `$Process.StandardError.ReadToEnd()
                Write-Log -Message "Error running takeown.exe `$Out"
            }
            else {
                [System.String]`$Out = `$Process.StandardOutput.ReadToEnd()
                Write-Log -Message "Successfully ran takeown - `$Out"
             
                [System.Diagnostics.Process]`$Process = New-Object System.Diagnostics.Process
                `$Process.StartInfo.Filename = "`$env:SYSTEMROOT\System32\icacls.exe"
                `$Process.StartInfo.Arguments = "```"$TargetFilePath```" /grant SYSTEM:f"
                `$Process.StartInfo.RedirectStandardOutput = `$true
                `$Process.StartInfo.UseShellExecute = `$false
                `$Process.StartInfo.CreateNoWindow = `$true
                `$Process.StartInfo.RedirectStandardError = `$true
                `$Process.Start() | Out-Null
                `$Process.WaitForExit()
 
                if (`$Process.ExitCode -ne 0) {
                    Write-Log -Message "icacls.exe exited with code `$Process.ExitCode."
                    [System.String]`$Out = `$Process.StandardError.ReadToEnd()
                    Write-Log -Message "Error running icacls.exe `$Out"
                }
                else {
                    [System.String]`$Out = `$Process.StandardOutput.ReadToEnd()
                    Write-Log -Message "Successfully ran icacls - `$Out"
                 
                    try {
                        Write-Log -Message "Renaming $TargetFilePath to $NewName."
                        Rename-Item -Path "$TargetFilePath" -NewName "$NewName"
                        Write-Log -Message "Copying new file from $FilePath to $TargetFilePath."
                        Copy-Item -Path "$FilePath" -Destination "$TargetFilePath" -Force
                        Start-Process -FilePath "`$env:SYSTEMROOT\System32\regsvr32.exe" -ArgumentList @("scecli.dll") -NoNewWindow
                        Write-Log -Message "Successfully registered new sceregvl.inf file."
                         
                        Write-Log -Message "Removing scheduled task, $TaskName."
                        Unregister-ScheduledTask -TaskName $TaskName -Confirm:`$false
                        Write-Log -Message "Successfully unregistered scheduled task."
                    }
                    catch [Exception] {
                        Write-Log `$_
                    }
                }
            }
        }
        catch [Exception] {
            Write-Log `$_
        }
"@


<# $Command = @"
        Start-Process -FilePath "`$env:SYSTEMROOT\System32\takeown.exe" -ArgumentList @("/f ```"$TargetFilePath```"") -NoNewWindow
        Start-Process -FilePath "`$env:SYSTEMROOT\System32\icacls.exe" -ArgumentList @("$TargetFilePath", "/grant SYSTEM:f") -NoNewWindow
        Rename-Item -Path "$TargetFilePath" -NewName "$NewName"
        Copy-Item -Path "$FilePath" -Destination "$TargetFilePath" -Force
        Start-Process -FilePath "`$env:SYSTEMROOT\System32\regsvr32.exe" -ArgumentList @("scecli.dll") -NoNewWindow
"@#>


        $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command)
        $EncodedCommand = [Convert]::ToBase64String($Bytes)

        $STParams = "-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -EncodedCommand $EncodedCommand"
        $STSource =  "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe"
        $STAction = New-ScheduledTaskAction -Execute $STSource -Argument $STParams
        $STPrincipal = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest
        $STSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew 
                          
        $ScheduledTask = Register-ScheduledTask -TaskName $TaskName -Action $STAction -Principal $STPrincipal -Settings $STSettings -ErrorAction Stop 

        Start-ScheduledTask -TaskName $TaskName

        if ($Wait) {
            $Task = $ScheduledTask

            $Counter = 0
            while ($Task -ne $null -and $Counter -lt 600) {
                $Task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
                Start-Sleep -Seconds 1
                $Counter++

                if ($Counter -eq 600) {
                    throw "Timeout waiting for scheduled task to replace sceregvl.inf to complete."
                }
            }
        }        
    }

    End{

    }
}

Function New-StandardForestGroups {
    <#
        .SYNOPSIS
            Creates the standard set of groups in the forest to manage the environment.
 
        .DESCRIPTION
            This cmdlet creates "pseudo" groups for the builtin administrative groups in Active Directory. This allows users to be added to these new groups and the builtin groups with well-known SIDs can be statically set
            so that any changes to those can undone or trigger an alert through monitoring tools.
 
        .PARAMETER GroupsJson
            A JSON string that defines the new groups and their membership. This defaults to empty and a standard set of groups is created based on the default OU configuration provided by the Set-OUStructure cmdlet.
 
        .PARAMETER FilePath
            The path to the JSON file that defines the new groups and their membership. This defaults to empty and a standard set of groups is created based on the default OU configuration provided by the Set-OUStructure cmdlet.
 
        .PARAMETER GroupNamePrefix
            The prefix of each group name if the default groups are used. This defaults to UG.
 
        .PARAMETER ResourceOUName
            The name of the OU under the top level domain that the User Resources OU is nested in. This is used with the default set of groups and is ignored if the filepath or json options are used. This option defaults to "Privileged Resources".
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADGroup[]
 
        .EXAMPLE
            New-StandardForestGroups -GroupNamePrefix "UG-ESAE" -ResourceOUName "ESAE Privileged Resources"
 
            Uses the default group definition and creates each group with the UG-ESAE prefix under the User Resources OU in the ESAE Privileged Resources OU at the top level of the domain.
 
            The results in groups like UG-ESAE-Domain-Admins.
 
        .EXAMPLE
            New-StandardForestGroups
 
            Uses the default group definition and creates each group with the UG prefix under the User Resources OU in the Privileged Resources OU at the top level of the domain.
 
            The results in groups like UG-Domain-Admins.
 
        .NOTES
            Refer to the Forest_LocalGroups.json file for appropriate syntax. The domain level should not be specified as part of the path for the group, it is derived programmatically.
    #>

    [CmdletBinding(DefaultParameterSetName="Default")]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="Json",Mandatory=$true)]
        [string]$GroupsJson = [System.String]::Empty,
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="File",Mandatory=$true)]        
        [string]$FilePath,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName="Default")]
        [string]$GroupNamePrefix = "UG",
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true,ParameterSetName="Default")]
        [string]$ResourceOUName = "Privileged Resources"

    )

    Begin {
        $TopLevelDomain = Get-ADDomain -Current LocalComputer | Select-Object -ExpandProperty DistinguishedName

        switch ($PSCmdlet.ParameterSetName)
        {
            "Default" {
                $GroupsJson = @"
[
    {
        "Name" : "$GroupNamePrefix-Domain-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Domain Admins"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name": "$GroupNamePrefix-GPO-Admins",
        "Scope": "Global",
        "Category": "Security",
        "MemberOf": [
          "Group Policy Creator Owners"
        ],
        "Path": "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-DNS-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "DnsAdmins"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Enterprise-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Enterprise Admins"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Schema-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Schema Admins"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Server-AD-LocalAdmins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Server Operators"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Server-LocalAdmins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Server Operators"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Workstation-LocalAdmins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Workstation-AD-LocalAdmins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Builtin-Administrators",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Administrators"
        ],
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Account-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "MemberOf" : [
            "Account Operators"
        ],
        "Path" :"OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    }
]
"@


                break
            }
            "File" {
                $GroupsJson = Get-Content -Path $FilePath -Raw
                break
            }
            "Json" {
                break
            }
            default {
                throw "Could not determine parameter set for New-StandardForestGroups."
            }
        }
        
        $Groups = ConvertFrom-Json -InputObject $GroupsJson
        $NewGroups = @()
    }

    Process {
        foreach ($Group in $Groups) {
            try
            {                
                #Test if group exists first
                $Temp = "CN=$($Group.Name),$($Group.Path),$TopLevelDomain"
                Write-Log -Message "Creating group $Temp."

                $NewGroup = Get-ADGroup -Filter {DistinguishedName -eq $Temp}

                if ($NewGroup -eq $null -or $NewGroup.Count -eq 0) {
                    $NewGroup = New-ADGroup -Name $Group.Name -DisplayName $Group.Name -Path "$($Group.Path),$TopLevelDomain" -SamAccountName $Group.Name -GroupCategory $Group.Category -GroupScope $Group.Scope -PassThru
                    if ($Group.MemberOf -ne $null -and $Group.MemberOf.Length -gt 0) {
                        foreach($Member in $Group.MemberOf) {
                            try {
                                Add-ADGroupMember -Identity $Member -Members $NewGroup
                            }
                            catch [Exception] {
                                Write-Warning ("Error adding $($NewGroup.Name) to $Member : " + $_.Exception.Message)
                                Write-Log ("Error adding $($NewGroup.Name) to $Member : " + $_.Exception.Message)
                            }
                        }
                    }
                }
                
                $NewGroups += $NewGroup
            }
            catch [Exception]
            {
                Write-Warning ("Error creating group : " + $_.Exception.Message)
                Write-Log ("Error creating group : " + $_.Exception.Message)
            }
        }
    }

    End {
        Write-Output $NewGroups
    }
}

Function New-ManagementGroups {
    <#
        .SYNOPSIS
            Creates the standard set of groups for an ESAE deployment to manage remote forests.
 
        .DESCRIPTION
            This cmdlet creates "pseudo" groups for the builtin administrative groups in Active Directory for remote forests. These groups can then be added to domain local groups in the remote forest to enable remote
            administration from the ESAE forest.
 
        .PARAMETER GroupsJson
            A JSON string that defines the new groups. This defaults to empty and a standard set of groups is created based on the default OU configuration provided by the Set-OUStructure cmdlet.
 
        .PARAMETER FilePath
            The path to the JSON file that defines the new groups. This defaults to empty and a standard set of groups is created based on the default OU configuration provided by the Set-OUStructure cmdlet.
 
        .PARAMETER GroupNamePrefix
            The prefix of each group name if the default groups are used. This defaults to UG, but should be set to something like UG-DomainName to specify the domain the group is intended to manage.
 
        .PARAMETER ResourceOUName
            The name of the OU under the top level domain that the User Resources OU is nested in. This is used with the default set of groups and is ignored if the filepath or json options are used. This option defaults to "Privileged Resources".
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADGroup[]
 
        .EXAMPLE
            New-ManagementGroups -GroupNamePrefix "UG-Constoso"
 
            Uses the default group definition and creates each group with the UG-Contoso prefix under the User Resources OU in the Privileged Resources OU at the top level of the domain.
 
            The results in groups like UG-Contoso-Domain-Admins.
 
        .NOTES
            Refer to the ESAE_RemoteManagementGroups.json file for appropriate syntax. The domain level should not be specified as part of the path for the group, it is derived programmatically.
    #>

    [CmdletBinding(DefaultParameterSetName="Json")]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="Json")]
        [string]$GroupsJson,
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="File",Mandatory=$true)]        
        [string]$FilePath,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName="Default")]
        [string]$GroupNamePrefix = "UG",
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true,ParameterSetName="Default")]
        [string]$ResourceOUName = "Privileged Resources"
    )

    Begin {
        $TopLevelDomain = Get-ADDomain -Current LocalComputer | Select-Object -ExpandProperty DistinguishedName

        switch ($PSCmdlet.ParameterSetName)
        {
            "Json" {
                if ($GroupsJson -eq $null -or $GroupsJson -eq [System.String]::Empty)
                {
                    $GroupsJson = @"
[
    {
        "Name" : "$GroupNamePrefix-Domain-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Enterprise-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Schema-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Server-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Administrators",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Workstation-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    },
    {
        "Name" : "$GroupNamePrefix-Account-Admins",
        "Scope" : "Global",
        "Category" : "Security",
        "Path" : "OU=User Groups,OU=User Resources,OU=$ResourceOUName"
    }
]
"@

                }

                break
            }
            "File" {
                $GroupsJson = Get-Content -Path $FilePath
                break
            }
            default {
                throw "Could not determine parameter set for New-ManagementGroups."
            }
        }

        $Groups = ConvertFrom-Json -InputObject $GroupsJson
        $NewGroups = @()
    }

    Process {
        foreach ($Group in $Groups) {
            try
            {
                if (!$Group.Path.EndsWith($TopLevelDomain)) {
                    $Path = ($Group.Path + "," + $TopLevelDomain)
                }
                else {
                    $Path = $Group.Path
                }

                #TEST if group exits first
                $Temp = "CN=$($Group.Name),$Path"
                $NewGroup = Get-ADGroup -Filter {DistinguishedName -eq $Temp}

                if ($NewGroup -eq $null) {
                    $NewGroup = New-ADGroup -Name $Group.Name -DisplayName $Group.Name -Path $Path -SamAccountName $Group.Name -GroupCategory $Group.Category -GroupScope $Group.Scope -PassThru
                }
                
                $NewGroups += $NewGroup
            }
            catch [Exception]
            {
                Write-Warning ("Error creating group $($Group.Name) " + $_.Exception.Message)
                Write-Log ("Error creating group $($Group.Name) " + $_.Exception.Message)
            }
        }
    }

    End {
        Write-Log "Finished creating Management groups."
        Write-Output $NewGroups
    }
}

#endregion

#region Build AD Structure

Function Set-OUStructure {
    <#
        .SYNOPSIS
            Creates an OU structure in the Active Directory forest.
 
        .DESCRIPTION
            Creates an OU structure in the Active Directory forest. This uses a default configuration if not JSON or file is specified. The structure is separated into two top level OUs, Privileged Resources that contain
            all of the users, groups, workstations and servers that are used to run the forest, and an Operations OU that contains all of the users, groups, workstations, and servers that are hosted in the Forest. The Operations
            OU is intended for non-privileged users, groups, etc that are tenants or consumers of services in AD, or for an ESAE deployment, only have administrative permissions in remote forests, but not the ESAE forest.
 
            The OU structure is created in the current domain of the computer that the cmdlet is being run on.
 
        .PARAMETER OUStructureJson
            A JSON string that defines the OU structure to be created. This defaults to empty and uses the default OU structure.
 
        .PARAMETER FilePath
            The path to the JSON file that defines the OU structure to be created.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADOrganizationalUnit[]
 
        .EXAMPLE
            Set-OUStructure
 
            Creates an OU structure in the current domain of the computer using the default setup.
 
        .NOTES
            The user needs Domain Admin permissions to create OUs in the domain.
 
            Refer to Forest_OU.json for the correct syntax of the JSON structure.
    #>

    [CmdletBinding(DefaultParameterSetName="Json")]
    Param (
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="Json")]
        [string]$OUStructureJson = [System.String]::Empty,
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="File",Mandatory=$true)]    
        [ValidateScript({Test-Path -Path $_})]    
        [string]$FilePath
    )

    Begin 
    {
        $TopLevelDomain = Get-ADDomain -Current LocalComputer | Select-Object -ExpandProperty DistinguishedName

        switch($PSCmdlet.ParameterSetName) {
            "Json" {
                if ([System.String]::IsNullOrEmpty($OUStructureJson)) {
                    $OUStructureJson = @"
[
    {
        "Label" : "Privileged Resources",
        "Children" : [
            {
                "Label" : "User Resources",
                "Children" : [
                    {
                        "Label" : "Users",
                        "Children" : []
                    },
                    {
                        "Label" : "User Groups",
                        "Children" : []
                    },
                    {
                        "Label" : "Workstations",
                        "Children" : []
                    },
                    {
                        "Label" : "Resource Groups",
                        "Children" : []
                    },
                    {
                        "Label" : "Service Accounts",
                        "Children" : []
                    }
                ]
            },
            {
                "Label" : "Server Resources",
                "Children" : [
                    {
                        "Label" : "Windows Servers",
                        "Children" : []
                    },
                    {
                        "Label" : "Linux Servers",
                        "Children" : []
                    },
                    {
                        "Label" : "Non-Windows Computer Objects",
                        "Children" : []
                    },
                    {
                        "Label" : "Resource Groups",
                        "Children" : []
                    }
                ]
            }
        ]
    },
    {
        "Label" : "Operations",
        "Children" : [
            {
                "Label" : "User Resources",
                "Children" : [
                    {
                        "Label" : "Users",
                        "Children" : []
                    },
                    {
                        "Label" : "User Groups",
                        "Children" : []
                    },
                    {
                        "Label" : "Workstations",
                        "Children" : []
                    },
                    {
                        "Label" : "Resource Groups",
                        "Children" : []
                    },
                    {
                        "Label" : "Service Accounts",
                        "Children" : []
                    }
                ]
            },
            {
                "Label" : "Server Resources",
                "Children" : [
                    {
                        "Label" : "Windows Servers",
                        "Children" : []
                    },
                    {
                        "Label" : "Linux Servers",
                        "Children" : []
                    },
                    {
                        "Label" : "Non-Windows Computer Objects",
                        "Children" : []
                    },
                    {
                        "Label" : "Resource Groups",
                        "Children" : []
                    }
                ]
            }
        ]
    }
]
"@

                }

                break
            }
            "File" {
                $OUStructureJson = Get-Content -Path $FilePath -Raw
                break
            }
            default {
                throw "Could not determine parameter set for New-OUStructure"
            }
        }

        try
        {
            $OUStructure = ConvertFrom-Json -InputObject $OUStructureJson
        }
        catch [Exception] 
        {
            throw $_.Exception
        }

        $NewOUs = @()
    }

    Process {
        foreach ($OU in $OUStructure) {
            $NewOUs += New-RecursiveOUStructure -Name $OU.Label -Path $TopLevelDomain -Children $OU.Children
        }
    }

    End {
        Write-Output $NewOUs
    }
}

Function New-RecursiveOUStructure {
    <#
        .SYNOPSIS
            Creates an OU structure recursively.
 
        .DESCRIPTION
            The cmdlet takes a name of a new OU, the path where it should be created, and any child OUs it should have. It creates the OU, and then calls itself for each defined child OU. All of the created OUs are returned
            in an array. If the OU happens to already exist, the existing OU is added to the array and the children continue to be processed.
 
        .PARAMETER Name
            The name of the new OU to create.
 
        .PARAMETER Path
            The parent path of where the new OU should be created. This is a Distinguished Name.
 
        .PARAMETER Children
            An array of OU objects to be created under this OU. Each Child object requires a Label property as its name and can have a Children property for additional nested OUs.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADOrganizationalUnit[]
 
        .EXAMPLE
            New-RecursiveOUStructure -Name "Privileged Resources" -Path "DC=admin,DC=local" -Children @(@{Label = "User Resources"; Children = @(@{Label = "Users"; Children = @()})})
 
            Creates an OU structure in the current domain of the computer. The Privileged Resources OU is created under admin.local. Then the User Resources OU is created under Privileged Resources, and the Users OU is created
            under the User Resources OU.
 
        .NOTES
            The user needs Domain Admin permissions to create OUs in the domain.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$Name,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$Path,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [Object[]]$Children
    )

    Begin {
        $NewOUs = @()
    }

    Process {
        if (![ADSI]::Exists("LDAP://OU=$Name,$Path")) {
            $NewOU = New-ADOrganizationalUnit -Name $Name -Path $Path -ProtectedFromAccidentalDeletion $true -PassThru
            $NewOUs += $NewOU
        }
        else {
            $NewOU = Get-ADOrganizationalUnit -Identity ("OU=$Name,$Path") -Properties DistinguishedName
        }

        if ($Children -ne $null -and $Children.Length -gt 0)
        {
            foreach ($Child in $Children) {
                $NewOUs +=    (New-RecursiveOUStructure -Name $Child.Label -Path $NewOU.DistinguishedName -Children $Child.Children)
            }
        }
    }

    End {
        Write-Output $NewOUs
    }
}

Function Import-ADUsers {
    <#
        .SYNOPSIS
            Imports new Active Directory users from a JSON file.
 
        .DESCRIPTION
            The cmdlet reads the contents of the JSON file, and creates users based on objects in the file. Any valid user class property can be defined. The path property defaults to the default Users container. If it is
            defined, it should not include the top level domain name, this is appended programmatically from the current domain of the computer running the cmdlet. Additionally, group memberships can be specified using the
            Membership property.
 
        .PARAMETER FilePath
            The path to the JSON file containing the users definition.
 
        .PARAMETER Json
            The JSON string defining the users to be created.
 
        .PARAMETER DefaultPassword
            Specifies the default password to assign to all the created users that must be changed on first logon. If no password is specified, a strong random password is generated which will need to be manually reset by
            an administrator before the user can logon.
 
        .PARAMETER EnableLogging
            Specifies if the logging function should be used when running the function.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Import-ADUsers -FilePath "$PSScriptRoot\users.json"
 
            Creates the users contained in the specified json file with a randomly generated password for each user.
 
        .NOTES
            Refer to the included Users.json file for syntax.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true,ParameterSetName="File",Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$FilePath,
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true,ParameterSetName="Json",Mandatory=$true)]
        [string]$Json,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [SecureString]$DefaultPassword = $null,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableLogging
    )

    Begin {
        if ($PSCmdlet.ParameterSetName -eq "File") {
            $Json = Get-Content -Path $FilePath -Raw
        }

        $Users = ConvertFrom-Json -InputObject $Json

        $Domain = Get-ADDomain -Current LoggedOnUser | Select-Object -Property *
        $TopLevel = $Domain.DistinguishedName
        $UsersContainer = $Domain.UsersContainer
        $Suffix = $Domain.DnsRoot

        [string[]]$UserClassProperties = @()

        [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]$Schema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
        [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass]$UserClass = $Schema.FindClass("User")
        $UserClassProperties += $UserClass.MandatoryProperties | Select-Object -ExpandProperty Name
        $UserClassProperties += $UserClass.OptionalProperties | Select-Object -ExpandProperty Name
        $UserClassProperties = $UserClassProperties.ToLower()

        $NotOtherAttrs = @("SamAccountName","GivenName","sn","DisplayName")
        $NotOtherAttrs = $NotOtherAttrs.ToLower()
    }

    Process {
        foreach ($User in $Users) {
            try {
                if (![System.String]::IsNullOrEmpty($User.GivenName) -and ![System.String]::IsNullOrEmpty($User.sn)) {

                    if ([System.String]::IsNullOrEmpty($User.SamAccountName)) {
                        $SamAccountName = "$($User.GivenName).$($User.sn)"
                    }
                    else {
                        $SamAccountName = $User.SamAccountName.Trim()
                    }

                    Write-Host "Processing $SamAccountName."
                    if ($EnableLogging) { Write-Log "Processing $SamAccountName." }

                    $SamAccountName = if ($SamAccountName.Length -gt 20) { $SamAccountName.Substring(0,20) } else {$SamAccountName}
            
                    $Counter = 0
                    $TempName = $SamAccountName
                    $NameSuffix = [System.String]::Empty

                    while ((Get-ADUser -Filter {samAccountName -eq $TempName}) -ne $null) {
                        if ($Counter -gt 999) {
                            Write-Log "SamAccountName suffix extension exceeded 999, cannot create $($User.SamAccountName) automatically."
                            throw "SamAccountName suffix extension exceeded 999, cannot create $($User.SamAccountName) automatically."
                        }

                        Write-Warning "$TempName already exists."
                        if ($EnableLogging) { Write-Log "$TempName already exists." }

                        $NameSuffix = $Counter.ToString()

                        while ($NameSuffix.Length -lt 3) {
                            $NameSuffix = "0" + $NameSuffix                
                        }
                
                        $TempName = if ($SamAccountName.Length -ge 16) { $SamAccountName.SubString(0,16) + ".$NameSuffix" } else { $SamAccountName + ".$NameSuffix" }

                        $Counter++
                    }

                    Write-Host "Using $TempName for the user."
                    if ($EnableLogging) { Write-Log "Using $TempName for the user." }

                    $SamAccountName = $TempName

                    if (![System.String]::IsNullOrEmpty($User.DisplayName)) {
                        $DisplayName = ($User.DisplayName + " " + $NameSuffix)
                    }
                    else {
                        $DisplayName =  "$($User.GivenName) $($User.sn) $NameSuffix"
                    }
                
                    $OtherAttrs = @{}
                
                    foreach ($Property in $User.psobject.Properties) {
                        if (!$NotOtherAttrs.Contains($Property.Name.ToLower()) -and $UserClassProperties.Contains($Property.Name.ToLower())) {
                            if ($Property.GetType() -eq [System.Array]) {
                                $Value = "@(" + ($Property.Value -join ",") + ")"
                            }
                            else {
                                $Value = $Property.Value.ToString()
                            }
               
                            $OtherAttrs.Add($Property.Name, $Value)
                        }
                    }
        
                    Write-Host "Creating user $SamAccountName"
                    if ($EnableLogging) { Write-Log "Creating user $SamAccountName" }

                    if ($DefaultPassword -eq $null) {
                        $DefaultPassword = New-RandomPassword -EnforceComplexity -AsSecureString
                    }

                    if ([System.String]::IsNullOrEmpty($User.Path)) {
                        $Path = $UsersContainer
                    } 
                    else {
                        $Path = "$($User.Path),$TopLevel"
                    }

                    try
                    {
                        $NewUser = New-ADUser -SamAccountName $SamAccountName `
                            -DisplayName $DisplayName.Trim() `
                            -GivenName $User.GivenName.Trim() `
                            -Surname $User.sn.Trim() `
                            -UserPrincipalName "$SamAccountName@$Suffix" `
                            -Name $DisplayName.Trim() `
                            -Path $Path `
                            -AccountPassword $DefaultPassword `
                            -PassThru
    
                        if ($NewUser) {
                            Set-ADUser -Identity $NewUser -Replace $OtherAttrs
                        }

                        foreach ($Group in $User.Membership) {
                            Write-Host "Checking $Group to add $SamAccountName."
                            if ($EnableLogging) { Write-Log "Checking $Group to add $SamAccountName." }
                    
                            if ((Get-ADGroup -Filter {name -eq $Group} -ErrorAction SilentlyContinue) -ne $null) {
                                try {
                                    Write-Host "$Group exists, attempting to add $SamAccountName."
                                    if ($EnableLogging) { Write-Log "$Group exists, attempting to add $SamAccountName." }
                                    Add-ADGroupMember -Identity $Group -Members $SamAccountName
                                    Write-Host "Successfully added $SamAccountName to $Group."
                                    if ($EnableLogging) { Write-Log "Successfully added $SamAccountName to $Group." }
                                }
                                catch [Exception] {
                                    Write-Warning $_.Exception.Message
                                    if ($EnableLogging) { Write-Log $_ }
                                }
                            }
                            else {
                                Write-Warning "$Group does not exist."
                                if ($EnableLogging) { Write-Log "$Group does not exist." }
                            }
                        }
                    }
                    catch [Exception] {
                        Write-Warning $_.Exception.Message
                        if ($EnableLogging) { Write-Log $_ }
                    }
                }
                else {
                    Write-Warning "The user object must have at least the GivenName and sn attributes defined."
                    if ($EnableLogging) { Write-Log ("The user object must have at least the GivenName and sn attributes defined.`n" + (ConvertFrom-Json -InputObject $User)) }
                }
            }
            catch [Exception] {
                Write-Warning $_.Exception.Message
                if ($EnableLogging) { Write-Log $_ }
            }
        }
    }

    End {

    }
}

#endregion

#region GPO Functions

Function Add-SysvolFiles {
    <#
        .SYNOPSIS
            Extracts the contents of a zip file and places them in the SYSVOL.
 
        .DESCRIPTION
            The cmdlet takes a source zip file and extracts the contents to a destination. The default destination is the SYSVOL as located from Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" | Select-Object -ExpandProperty SysVol.
            However, this property is only available on domain controllers, so if the command is run remotely, the SYSVOL destination needs to be explicitly set.
 
        .PARAMETER ZipFilePath
            The path to the source zip file. This defaults to $PSScriptRoot\SysvolFiles.zip.
 
        .PARAMETER Destination
            The location where the zip file should be extracted to. This defaults to the SYSVOL when run on a domain controller.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Add-SysvolFiles
 
            Extracts the contents of $PSScriptRoot\SysvolFiles.zip to the SYSVOL directory of the current forest root domain of the computer the cmdlet is being run on.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({Test-Path -Path $_})]        
        [string]$ZipFilePath = $script:SysvolFiles,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$Destination = [System.String]::Empty
    )

    Begin {
        $ForestRootDomainName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name

        if ([System.String]::IsNullOrEmpty($Destination)) {
            $SYSVOL = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" | Select-Object -ExpandProperty SysVol
            $Destination = "$SYSVOL\$ForestRootDomainName\files"
        }
    }

    Process {
        Write-Log "Extracting $ZipFilePath to $Destination"
        Extract-ZipFile -Source $ZipFilePath -Destination $Destination
    }

    End {
    }
}

Function Set-GPPMigrationValues {
    <#
        .SYNOPSIS
            The cmdlet updates the values in Group Policy Preferences to replace any references of the source domain with the destination domain.
 
        .DESCRIPTION
            The cmdlet examines each GPO backup in the backup directory specified and searches GPPs for references to the source domain. It replaces any found matches with the forest root domain name.
 
        .PARAMETER BackupRootDirectory
            The top level folder containing GPO backups.
 
        .PARAMETER SourceDomain
            The name of the domain where the GPO backups were originated from. This defaults to admin.local.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-GPPMigrationValues -BackupRootDirectory "c:\gpobackups"
 
            Replaces all references of admin.local in GPO backups located in c:\gpobackups for GPPs.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]    
        [ValidateScript({Test-Path -Path $_})]    
        [string]$BackupRootDirectory,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$SourceDomain = "admin.local"
    )

    Begin {
        try {
            Import-Module GroupPolicy
        }
        catch [Exception] {
            throw "The GroupPolicy module must be installed to use this cmdlet."
        }
        
        if ([System.String]::IsNullOrEmpty($SourceDomain)) {
            $SourceDomain = "admin.local"
        }

        $ForestRootDomainName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name
        $Netbios = $ForestRootDomainName.Substring(0, $ForestRootDomainName.IndexOf("."))

        if ($SourceDomain.Contains(".")) {
            $SourceNetbios = $SourceDomain.Substring(0, $SourceDomain.IndexOf("."))
        }
        else {
            throw "The source domain name was not properly formatted."
        }
    }

    Process {
        $Directories = Get-ChildItem -Path $BackupRootDirectory -Recurse -Directory | Select-Object -ExpandProperty FullName

        foreach ($Directory in $Directories)
        {
            [Microsoft.GroupPolicy.BackupDirectory]$BackupDirectory = New-Object Microsoft.GroupPolicy.BackupDirectory($Directory, [Microsoft.GroupPolicy.BackupType]::Gpo)
            [Microsoft.GroupPolicy.GPSearchCriteria]$SearchCriteria = New-Object Microsoft.GroupPolicy.GPSearchCriteria
            $SearchCriteria.Add([Microsoft.GroupPolicy.SearchProperty]::MostRecentBackup, [Microsoft.GroupPolicy.SearchOperator]::Equals, $true)
            [Microsoft.GroupPolicy.GpoBackupCollection]$Backups = $BackupDirectory.SearchGpoBackups($SearchCriteria)

            if ($Backups -ne $null -and $Backups.Count -gt 0)
            {
                foreach($Backup in $Backups)
                {
                    $Items = @()
                    $MachineGPP = [System.IO.Path]::Combine($Backup.BackupDirectory, "DomainSysvol", "GPO", "Machine", "Preferences")
                    $UserGPP = [System.IO.Path]::Combine($Backup.BackupDirectory, "DomainSysvol", "GPO", "User", "Preferences")
                    $Netbios = $ForestRootDomainName.Substring(0, ([System.Math]::Min($ForestRootDomainName.IndexOf("."), 15)))

                    if (Test-Path -Path $MachineGPP) {
                        $Items += Get-ChildItem -Path $MachineGPP -File
                    }

                    if (Test-Path -Path $UserGPP) {
                        $Items += Get-ChildItem -Path $UserGPP -File
                    }

                    foreach($Item in $Items) {
                        $StrContent = Get-Content -Path $Item.FullName -Raw

                        $StrContent = $StrContent -replace $SourceDomain,$ForestRootDomainName #Replace any reference of the default domain name
                        $StrContent = $StrContent -replace "\b@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b","@$Netbios" #Replace any UPN type entries
                        $Strcontent = $StrContent -replace ([System.Text.RegularExpressions.Regex]::Escape("$SourceNetbios\")),("$Netbios\")

                        Set-Content -Value $StrContent -Path $Item.FullName
                    }
                }
            }
        }
    }

    End {

    }
}

Function New-GPOMigrationTable {
    <#
        .SYNOPSIS
            The cmdlet creates a GPO migration table to replace any references with the source domain of the GPO backups with the destination domain.
 
        .DESCRIPTION
            The cmdlet creates a GPO migration table to replace any references with the source domain of the GPO backups with the destination domain.
 
        .PARAMETER BackupRootDirectory
            The top level folder containing GPO backups.
 
        .PARAMETER SourceDomain
            The name of the domain where the GPO backups were originated from. This defaults to admin.local.
 
        .PARAMETER DestinationDomain
            The name of the new domain where the GPOs will be imported. This defaults to the forest root domain of the computer running the cmdlet.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            System.String
            The migration table path.
 
        .EXAMPLE
            New-GPOMigrationTable -BackupRootDirectory "c:\gpobackups"
 
            Creates a migration table for all of the gpo backups in the c:\gpobackups folder.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({Test-Path -Path $_})]        
        [string]$BackupRootDirectory,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$DestinationDomain = [System.String]::Empty,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({$_.Contains(".")})]
        [string]$SourceDomain = "admin.local"
    )

    Begin {
        try {
            Import-Module GroupPolicy
        }
        catch [Exception] {
            throw "The GroupPolicy module must be installed to use this cmdlet."
        }

        if ([System.String]::IsNullOrEmpty($DestinationDomain)) {
            $ForestRootDomainName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name
        }
        else {
            $ForestRootDomainName = $DestinationDomain
        }

        if ($ForestRootDomainName.Contains(".")) {
            $Netbios = $ForestRootDomainName.Substring(0, $ForestRootDomainName.IndexOf("."))
        }
        else {
            throw "The destination domain name was not properly formatted."
        }

        $GPM = New-Object -ComObject GPMgmt.GPM
        $Constants = $GPM.GetConstants()

        if ([System.String]::IsNullOrEmpty($SourceDomain)) {
            $SourceDomain = "admin.local"
        }
    }

    Process {
        
        $MigrationTable = New-Object Microsoft.GroupPolicy.GPMigrationTable
        $Directories = Get-ChildItem -Path $BackupRootDirectory -Recurse -Directory | Select-Object -ExpandProperty FullName

        foreach ($Directory in $Directories)
        {
            [Microsoft.GroupPolicy.BackupDirectory]$BackupDirectory = New-Object Microsoft.GroupPolicy.BackupDirectory($Directory, [Microsoft.GroupPolicy.BackupType]::Gpo)
            [Microsoft.GroupPolicy.GPSearchCriteria]$SearchCriteria = New-Object Microsoft.GroupPolicy.GPSearchCriteria
            $SearchCriteria.Add([Microsoft.GroupPolicy.SearchProperty]::MostRecentBackup, [Microsoft.GroupPolicy.SearchOperator]::Equals, $true)
            [Microsoft.GroupPolicy.GpoBackupCollection]$Backups = $BackupDirectory.SearchGpoBackups($SearchCriteria)

            if ($Backups -ne $null -and $Backups.Count -gt 0)
            {
                foreach($Backup in $Backups)
                {
                    $MigrationTable.Add($Backup, $false)
                }
            }
        }

        foreach($Entry in $MigrationTable.GetEntries()) {
            Write-Host $Entry.Source
            switch ($Entry.EntryType) {
                $Constants.EntryTypeUNCPath {
                    Write-Host "UNC Path."
                    if ($Entry.Source -like ".*$SoureDomain.*") {
                        $MigrationTable.UpdateDestination($Entry.Source, ($Entry.Source -replace $SourceDomain,$ForestRootDomainName)) | Out-Null

                        $UpdatedEntry = $MigrationTable.GetEntry($Entry.Source)
                        Write-Host "Updated UNC Path Entry $($Entry.Source) to $($UpdatedEntry.Destination)"
                    }
                    break
                }
                {$_ -in $Constants.EntryTypeUser, $Constants.EntryTypeGlobalGroup, $Constants.EntryTypeUnknown} {
                    Write-Host "User, Global Group, or Unknown."
                    if ($Entry.Source -match "\b@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b") {
                        $MigrationTable.UpdateDestination($Entry.Source, ($Entry.Source -replace "\b@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b","@$ForestRootDomainName")) | Out-Null

                        $UpdatedEntry = $MigrationTable.GetEntry($Entry.Source)
                        Write-Host "Updated User/Global Group/Unknown Entry $($Entry.Source) to $($UpdatedEntry.Destination)"
                    }
                    break
                }
                {$_ -in $Constants.EntryTypeUniversalGroup} {
                    Write-Host "Universal Group"
                    if ($Entry.Source -match "\b@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b") {
                        $MigrationTable.UpdateDestination($Entry.Source, ($Entry.Source -replace "\b@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b","@$ForestRootDomainName")) | Out-Null

                        $UpdatedEntry = $MigrationTable.GetEntry($Entry.Source)
                        Write-Host "Updated Universal Group Entry $($Entry.Source) to $($UpdatedEntry.Destination)"
                    }
                    break
                }
            }
        }
    }

    End {
        $MigrationTablePath = [System.IO.Path]::Combine($PSScriptRoot,"MigrationTable.migtable")
        $MigrationTable.Save($MigrationTablePath)

        Write-Output $MigrationTablePath
    }
}

Function New-GPOLinks {
    <#
        .SYNOPSIS
            The cmdlet creates GPO links for existing GPOs to specified OUs.
 
        .DESCRIPTION
            The cmdlet processes an inputted json file that defines the GPOs and where each one should be linked. If the GPO or destination for the link does not exist it is ignored.
 
        .PARAMETER LinksFilePath
            The path to the json file containing the link information. The OU property of each link object should not include the top level domain, this is generated programmatically. Leave the property blank to link the GPO
            to the domain. The GPO property is the name of the GPO, these are matched with an implicit wildcard at the end of the name, so multiple GPOs can be linked with just one definition.
 
            You must define the GPO and Path. You can optionally define "Enforced" as "Yes" or "No" and optionally define "Order" as an integer value.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            New-GPOLinks -LinksFilePath "$PSScriptRoot\Forest_GPLinks.json"
 
            Links all of the GPOs defined in the Forest_GPLinks.json file.
 
        .NOTES
            Refer to the Forest_GPLinks.json or ESAE_GPLinks.json file for correct syntax.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$LinksFilePath
    )

    Begin {
        try {
            Import-Module GroupPolicy
        }
        catch [Exception] {
            Write-Log "The group policy module is required to run this command."
            throw $_.Exception
        }

        if (!(Test-Path -Path $LinksFilePath)) {
            throw [System.IO.FileNotFoundException]("$LinksFilePath could not be found.")
        }

        $Links = ConvertFrom-Json -InputObject (Get-Content -Path $LinksFilePath -Raw)

        $DnsName = Get-ADForest -Current LocalComputer | Select-Object -ExpandProperty Name
        $TopLevelDomain = Get-ADDomain -Identity $DnsName | Select-Object -ExpandProperty DistinguishedName

        [Object[]]$AllGPOs = Get-GPO -All
    }

    Process {

        foreach ($Link in $Links) {
            Write-Host "Processing GPOs that match $($Link.GPO)"
            Write-Log "Processing GPOs that match $($Link.GPO)"

            $Subset = $AllGPOs | Where-Object {$_.DisplayName -like "$($Link.GPO)*"}
            
            if($Link.Enforced -eq $null) {
                $Enforced = "No"
            }
            else {
                $Enforced = $Link.Enforced
            }

            if ([System.String]::IsNullOrEmpty($Link.OU)) {
                $Target = $TopLevelDomain
            }
            else {
                $Target = "$($Link.OU),$TopLevelDomain"
            }

            if ($Subset.Length -gt 0) {
                foreach($Item in $Subset) {
                    try {
                        Write-Host "Linking $($Item.DisplayName) to $Target."
                        Write-Log "Linking $($Item.DisplayName) to $Target."

                        if ($Link.Order -ne $null) {
                            New-GPLink -Guid $Item.Id -Target $Target -LinkEnabled Yes -Enforced $Enforced -Order $Link.Order -Server $env:COMPUTERNAME -Domain $DnsName
                        }
                        else {
                            New-GPLink -Guid $Item.Id -Target $Target -LinkEnabled Yes -Enforced $Enforced -Server $env:COMPUTERNAME -Domain $DnsName
                        }
                    }
                    catch [Exception] {
                        Write-Warning $_.Exception.Message
                        Write-Log $_
                    }
                }
            }
            else {
                Write-Warning "The GPO $($Link.GPO) does not exist in the domain and could not be linked to $($Link.OU)."
                Write-Log "The GPO $($Link.GPO) does not exist in the domain and could not be linked to $($Link.OU)."
            }
        }
    }

    End {
        Write-Host "Completed linking GPOs."
    }
}

#endregion

#region Forest Trusts

Function New-ADForestTrust {
    <#
        .SYNOPSIS
            The cmdlet builds a forest trust between two forests.
 
        .DESCRIPTION
            The cmdlet creates a forest trust between two forests and configures settings associated with the trust. The command will remove existing trust objects with the same name if they are discovered. Because the
            Microsoft function for trusts only checks the NetBIOS name when it checks for existence, a trust for another forest could inadvertently be removed. For example, if the local forest is admin.local and the remote
            forest has a trust to admin.com, building the trust would fail.
 
        .PARAMETER LocalForest
            The name of the local forest where the cmdlet is being run. This defaults to the forest root domain of the computer running the cmdlet.
 
        .PARAMETER RemoteForest
            The name of the remote forest where the trust will connect.
 
        .PARAMETER TrustDirection
            The direction of the forest trust in relation to the server the cmdlet is being run on. This defaults to Inbound (the remote forest trusts the local forest). This can be Inbound, Outbound, or Bidirectional.
 
        .PARAMETER LocalForestCredential
            The credential to use to setup the local side of the trust. The credential should have Enterprise Admin rights and defaults to the user running the cmdlet.
 
        .PARAMETER RemoteForestCredential
            The credential to use to setup the remote side of the trust. The credential should have Enterprise Admin rights in the remote forest.
 
        .PARAMETER EnableSelectiveAuthentication
            Specify whether selective authentication is enabled or not. This defaults to true.
 
        .PARAMETER SidFilteringEnabled
            Specify whether SID filtering is enabled or not. This defaults to true.
 
        .PARAMETER CreateLocalConditionalForwarder
            Specifies whether to create a DNS conditional forwarder in the local forest in order to resolve the remote forest DNS name.
 
        .PARAMETER RemoteForestMasterServers
            The IP addresses of the remote forest DNS servers that the local conditional forwarder will point to. This is required if the CreateLocalConditionalForwarder is specified.
 
        .PARAMETER CreateRemoteConditionalForwarder
            Specifies whether to create a DNS conditional forwarder in the remote forest in order to resolve the local forest DNS name.
 
        .PARAMETER TrustingDomainSupportsKerberosAESEncryption
            Specifies whether the trusting domain supports Kerberos AES Encryption. This defaults to true.
 
        .PARAMETER EnableLogging
            Specify if the module logging function should be used when running the cmdlet.
 
        .PARAMETER WaitForRpcSs
            Specify whether to wait for the RPCSS service to be available on the local server before creating the DNS settings. The RPCSS service is sometime not available immediately after a reboot when the cmdlet is being
            run as a scheduled task and the DNS configuration fails.
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            New-ADForestTrust -RemoteForest "contoso.com" -RemoteForestCredential (Get-Credential) -CreateLocalConditionalForwarder -CreateRemoteConditionalForwarder -RemoteForestMasterServers @("192.168.2.1") -WaitForRpcSs
 
            Creates conditional forwarders in both the remote and local forest and establishes and inbound forest trust.
 
        .NOTES
            None
    #>

    [CmdletBinding(DefaultParameterSetName="None")]
    Param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true)]
        [string]$LocalForest = [System.String]::Empty,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$RemoteForest,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [System.DirectoryServices.ActiveDirectory.TrustDirection]$TrustDirection = [System.DirectoryServices.ActiveDirectory.TrustDirection]::Inbound,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true)]
        [PSCredential]$LocalForestCredential = [System.Management.Automation.PSCredential]::Empty,
        [Parameter(Position=4,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [PSCredential]$RemoteForestCredential,
        [Parameter(Position=5,ValueFromPipelineByPropertyName=$true)]
        [bool]$EnableSelectiveAuthentication = $true,
        [Parameter(Position=6,ValueFromPipelineByPropertyName=$true)]
        [bool]$SidFilteringEnabled = $true,
        [Parameter(Position=7,ValueFromPipelineByPropertyName=$true,ParameterSetName="StubZone")]
        [switch]$CreateLocalConditionalForwarder = $false,
        [Parameter(Position=8,ValueFromPipelineByPropertyName=$true,ParameterSetName="StubZone",Mandatory=$true)]
        [string[]]$RemoteForestMasterServers,
        [Parameter(Position=9,ValueFromPipelineByPropertyName=$true)]
        [switch]$CreateRemoteConditionalForwarder = $false,
        [Parameter(Position=10,ValueFromPipelineByPropertyName=$true)]
        [bool]$TrustingDomainSupportsKerberosAESEncryption = $true,
        [Parameter(Position=11,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableLogging,
        [Parameter(Position=12,ValueFromPipelineByPropertyName=$true)]
        [switch]$WaitForRpcSs
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($LocalForest)) {
            $LocalForest = (Get-ADForest -Current LoggedOnUser).DnsRoot
        }

        if ($CreateLocalConditionalForwarder -and $RemoteForestMasterServers.Count -lt 1) {
            if ($EnableLogging) { Write-Log "The create local conditional forwarder parameter was specified, but no remote master servers were specified." }
            throw "The create local conditional forwarder was specified, but no remote master servers were specified."
        }
    }

    Process {

        if ($LocalForestCredential -ne $null -and $LocalForestCredential -ne [PSCredential]::Empty) {
            try
            {
                [System.DirectoryServices.ActiveDirectory.DirectoryContext]$LocalForestContext = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest, $LocalForest, $LocalForestCredential.UserName, (Convert-SecureStringToString -SecureString $LocalForestCredential.Password))
                [System.DirectoryServices.ActiveDirectory.Forest]$LocalForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($LocalForestContext)
            }
            catch [Exception] {
                Write-Warning $_.Exception
                if ($EnableLogging) { Write-Log $_ }
                throw $_.Exception
            }
        }
        else {
            try {
                [System.DirectoryServices.ActiveDirectory.Forest]$LocalForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            }
            catch [Exception] {
                Write-Warning $_.Exception
                if ($EnableLogging) { Write-Log $_ }
                throw $_.Exception
            }
        }

        $Continue = $true
        
        if ($CreateLocalConditionalForwarder) {
            try {
                if ((Test-RpcAvailability -ComputerName $LocalForest.Name -EnableLogging -Wait:$WaitForRpcSs)) {

                    Write-Host "RPC available, testing for the stub zone existing already."
                    if ($EnableLogging) { Write-Log "RPC available, testing for the stub zone existing already." }

                    if ((Get-DnsServerZone -Name $RemoteForest -ErrorAction SilentlyContinue) -eq $null) {
                        Write-Host "Zone does not exist, creating conditional forwarder for remote forest $RemoteForest."
                        if ($EnableLogging) { Write-Log "Zone does not exist, creating conditional forwarder for remote forest $RemoteForest." }

                        try 
                        {                        
                            $Counter = 0
                            while ($true) {
                                try {
                                    Add-DnsServerConditionalForwarderZone -Name $RemoteForest -MasterServers $RemoteForestMasterServers -ReplicationScope Forest -ComputerName $LocalForest.Name
                                    Write-Host "Conditional forwarder $RemoteForest successfully created."
                                    if ($EnableLogging) { Write-Log "Conditional forwarder $RemoteForest successfully created." }
                                    break
                                }
                                catch [Exception] {
                                    if ($Counter -gt 10) {
                                        throw $_.Exception
                                    }
                                    else {                                        
                                        Write-Warning "Failed to create conditional forwarder on attempt $($Counter + 1)."
                                        Write-Warning "$($_.Exception.Message)"
                                        if ($EnableLogging) { Write-Log "Failed to create conditional forwarder on attempt $($Counter + 1)."}
                                        if ($EnableLogging) { Write-Log $_ }
                                        $Counter++
                                        Start-Sleep -Seconds 10
                                    }
                                }
                            }
                        }
                        catch [Exception] {
                            Write-Warning $_.Exception.Message
                            if ($EnableLogging) { Write-Log $_ }
                        }
                    }
                    else {
                        Write-Warning "Zone $RemoteForest already exists."
                        if ($EnableLogging) { Write-Log "Zone $RemoteForest already exists."}
                    }
                }
                else {
                    if ($EnableLogging) { Write-Log "RpcSs service not available on $($LocalForest.Name)."}
                    throw "RpcSs service not available on $($LocalForest.Name)."
                }
            }
            catch [Exception] {
                Write-Warning $_.Exception.Message
                if ($EnableLogging) { Write-Log $_ }
                $Continue = $false
            }
        }

        try {
            Write-Host "Ensuring the remote forest name provided is actually the forest root."
            if ($EnableLogging) { Write-Log "Ensuring the remote forest name provided is actually the forest root." }
            $RemoteForest = (Get-ADDomain -Identity $RemoteForest -Credential $RemoteForestCredential).Forest
            Write-Host "Forest root is $RemoteForest."
            if ($EnableLogging) { Write-Log "Forest root is $RemoteForest." }
        }
        catch [Exception] {
            Write-Warning $_.Exception.Message
            if ($EnableLogging) { Write-Log "Error getting forest root."}
            if ($EnableLogging) { Write-Log $_ }
            $Continue = $false
        }

        if ($CreateRemoteConditionalForwarder -and $Continue) {
            try
            {
                $Servers = Resolve-DnsName -Name "_ldap._tcp.dc._msdcs.$RemoteForest" -Type SRV | Where-Object {$_.Type -eq "SRV"} | Select-Object -ExpandProperty NameTarget 
                $RemoteServer = [System.String]::Empty

                foreach ($Server in $Servers) {
                    Write-Host "Testing connection to $Server."
                    if ($EnableLogging) { Write-Log "Testing connection to $Server." }
                    if (Test-Connection -ComputerName $Server -Quiet) {
                        Write-Host "Successfully connected to $Server."
                        if ($EnableLogging) { Write-Log "Successfully connected to $Server." }
                        $RemoteServer = $Server
                        break
                    }
                    else {
                        Write-Warning "Could not connect to $Server."
                        if ($EnableLogging) { Write-Log "Could not connect to $Server." }
                    }
                }

                if (![System.String]::IsNullOrEmpty($RemoteServer)) {

                    Write-Host "Creating conditional forwarder in remote forest on server $RemoteServer."
                    if ($EnableLogging) { Write-Log "Creating conditional forwarder in remote forest on server $RemoteServer."}
                    Write-Host "Getting list of master servers in $($LocalForest.Name) to create in remote forest conditional forwarder."
                    if ($EnableLogging) { Write-Log "Getting list of master servers in $($LocalForest.Name) to create in remote forest conditional forwarder." }
                    $LocalMasterServers = @()

                    Resolve-DnsName -Name $LocalForest.Name -Type NS | Where-Object {![System.String]::IsNullOrEmpty($_.NameHost)} | Select-Object -ExpandProperty NameHost | ForEach-Object {
                        $LocalMasterServers += Resolve-DnsName -Name $_ -Type A | Select-Object -ExpandProperty IPAddress
                    }

                    Write-Host ("Master Servers: " + ($LocalMasterServers -join " "))
                    if ($EnableLogging) { Write-Log ("Master Servers: " + ($LocalMasterServers -join " ")) }

                    #Create DNS StubZone for Trusting Forest so it can identify the trusted forest
                    Write-Host "Creating remote conditional forwarder pointing to the $($LocalForest.Name)."
                    if ($EnableLogging) { Write-Log "Creating remote conditional forwarder pointing to $($LocalForest.Name)." }

                    Write-Host "Connecting to $RemoteServer."
                    if ($EnableLogging) { Write-Log "Connecting to $RemoteServer." }

                    if (Test-Connection -ComputerName $RemoteServer -Quiet) {
                
                        $Session = New-PSSession -ComputerName $RemoteServer -Credential $RemoteForestCredential -ErrorAction Stop

                        $Result = Invoke-Command -Session $Session -ScriptBlock {
                            $Zone = Get-DnsServerZone -Name $args[0] -ErrorAction SilentlyContinue
                            if ($Zone -eq $null) {
                                $Zone = Add-DnsServerConditionalForwarderZone -Name $args[0] -MasterServers $args[1] -ReplicationScope Forest -PassThru

                                Write-Output $Zone
                            }
                            else {
                                switch ($Zone.Type) {
                                    "Forwarder" {
                                        $Zone = Set-DnsServerConditionalForwarderZone -Name $args[0] -MasterServers $args[1] -PassThru
                                        break
                                    }
                                    "Stub" {
                                        $Zone = Set-DnsServerStubZone -Name $args[0] -MasterServers $args[1] -PassThru
                                        break
                                    }
                                    default {
                                        break
                                    }
                                }

                                Write-Output $Zone

                            }
                        } -ErrorVariable ErrResult -ArgumentList @($LocalForest.Name, $LocalMasterServers)

                        Remove-PSSession -Session $Session

                        if ($ErrResult -ne $null -and $ErrResult.Count -gt 0) {
                            $Err = $ErrResult[0]
                            throw $Err.Exception
                        }
                        else {
                            Write-Host "Successfully created remote forest conditional forwarder."
                            Write-Host ($Result | Format-List)
                            if ($EnableLogging) { Write-Log "Successfully created remote forest conditional forwarder."}
                            if ($EnableLogging) { Write-Log ($Result | Format-List | Out-String) }
                        }
                    }
                    else {
                        Write-Warning ("Could not connect to $RemoteServer.")
                        if ($EnableLogging) { Write-Log "Could not connect to $RemoteServer." }
                        $Continue = $false
                    }
                }
                else {
                    Write-Warning "Could not connect to any servers resolved from DNS."
                    if ($EnableLogging) { "Could not connect to any servers resolved from DNS." }
                    $Continue = $false
                }
            }
            catch [Exception] {
                Write-Warning $_.Exception.Message
                if ($EnableLogging) { Write-Log $_ }
                $Continue = $false
            }
        }
        
        if ($Continue) {
            try {
                #Now that the local conditional forwarder exists and the DC can resolve the other domain name, build the remote forest context items
    
                try {
                    [System.DirectoryServices.ActiveDirectory.DirectoryContext]$RemoteForestContext = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest, $RemoteForest, $RemoteForestCredential.UserName, (Convert-SecureStringToString -SecureString $RemoteForestCredential.Password))
                    [System.DirectoryServices.ActiveDirectory.Forest]$RemoteForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($RemoteForestContext)
                }
                catch [Exception] {
                    Write-Warning $_.Exception.Message
                    if ($EnableLogging) { Write-Log $_ }
                    throw $_.Exception
                }

                $LocalNetbios = $LocalForest.Name.Substring(0, $LocalForest.Name.IndexOf("."))
                $RemoteNetbios = $RemoteForest.Name.Substring(0, $RemoteForest.Name.IndexOf("."))

                #Assume trust exists on the far side and prove otherwise
                $RemoteExists = $true
                if ($EnableLogging) { Write-Log "Testing for remote trust existence."}

                try {
                    $RemoteTrust = Get-ADForestTrustRelationship -TargetForestName $LocalNetbios -CurrentForest $RemoteForest
                    if ($RemoteTrust -eq $null) {
                        $RemoteExists = $false
                    }
                    else {
                        $RemoteExists = $true
                    }
                }
                catch [Exception] {
                    Write-Warning $_.Exception.Message
                    if ($EnableLogging) { Write-Log $_ }
                }

                Write-Host "Remote trust exists: $RemoteExists for Netbios $LocalNetbios."
                if ($EnableLogging) { Write-Log "Remote trust exists: $RemoteExists for Netbios $LocalNetbios"}

                #Assume the local trust exists and prove otherwise
                $LocalExists = $true
                if ($EnableLogging) { Write-Log "Testing for local trust existence."}

                try {
                    $LocalTrust = Get-ADForestTrustRelationship -TargetForestName $RemoteNetbios -CurrentForest $LocalForest

                    if ($LocalTrust -eq $null) {
                        $LocalExists = $false
                    }
                    else {
                        $LocalExists = $true
                    }
                }
                catch [Exception] {
                    Write-Warning $_.Exception.Message
                    if ($EnableLogging) { Write-Log $_ }
                }

                Write-Host "Local trust exists: $LocalExists"
                if ($EnableLogging) { Write-Log "Local trust exists: $LocalExists"}

                if ($RemoteExists -and $LocalExists) {
                    Write-Host "Deleting existing trust relationship."
                    if ($EnableLogging) { Write-Log "Deleting existing trust relationship."}                    
                    $Counter = 0

                    while ($true) {
                        try {
                            $LocalForest.DeleteTrustRelationship($RemoteForest)
                            Write-Host "Successfully deleted existing trust relationship."
                            if ($EnableLogging) { Write-Log "Successfully deleted existing trust relationship."}
                            break
                        }
                        catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
                            Write-Warning $_.Exception.Message
                            if ($EnableLogging) { Write-Log $_ }
                            throw $_.Exception
                        }    
                        catch [Exception] {
                            if ($Counter -gt 60) {
                                throw $_.Exception
                            }
                            else {
                                if ($EnableLogging) { Write-Log "Failed to delete the trust on attempt $($Counter + 1)."}
                                if ($EnableLogging) { Write-Log $_ }
                                Start-Sleep -Seconds 10
                                $Counter++
                            }
                        }    
                    }
                }

                if ($RemoteExists -and !$LocalExists) {
                    Write-Host "Deleting local side of remote forest trust in relation to the remote forest."
                    if ($EnableLogging) { Write-Log "Deleting local side of remote forest trust in relation to the remote forest."}
                    $Counter = 0

                    while ($true) {
                        try {
                            $RemoteForest.DeleteLocalSideOfTrustRelationship($LocalNetbios)
                            Write-Host "Successfully deleted local side of remote forest trust."
                            if ($EnableLogging) { Write-Log "Successfully deleted local side of remote forest trust."}
                            break
                        }
                        catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
                            Write-Warning $_.Exception.Message
                            if ($EnableLogging) { Write-Log $_ }
                            throw $_.Exception
                        }
                        catch [Exception] {
                            if ($Counter -gt 60) {
                                throw $_.Exception
                            }
                            else {
                                if ($EnableLogging) { Write-Log "Failed to delete the trust on attempt $($Counter + 1)."}
                                if ($EnableLogging) { Write-Log $_ }
                                Start-Sleep -Seconds 10
                                $Counter++
                            }
                        }    
                    }
                }

                if (!$RemoteExists -and $LocalExists) {
                    Write-Host "Deleting local side of local forest trust."
                    if ($EnableLogging) { Write-Log "Deleting local side of local forest trust."}
                    $Counter = 0    

                    while ($true) {
                        try {
                            $LocalForest.DeleteLocalSideOfTrustRelationship($RemoteNetbios)
                            Write-Host "Successfully deleted local side of local forest trust."
                            if ($EnableLogging) { Write-Log "Successfully deleted local side of local forest trust."}
                            break
                        }
                        catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
                            Write-Warning $_.Exception.Message
                            if ($EnableLogging) { Write-Log $_ }
                            throw $_.Exception
                        }
                        catch [Exception] {
                            if ($Counter -gt 60) {
                                throw $_.Exception
                            }
                            else {
                                if ($EnableLogging) { Write-Log "Failed to delete the trust on attempt $($Counter + 1)."}
                                if ($EnableLogging) { Write-Log $_ }
                                Start-Sleep -Seconds 10
                                $Counter++
                            }
                        }    
                    }
                }

                Write-Host "Creating trust relationship between $($LocalForest.Name) and $($RemoteForest.Name) in the direction of $($TrustDirection.ToString())."
                if ($EnableLogging) { Write-Log "Creating trust relationship between $($LocalForest.Name) and $($RemoteForest.Name) in the direction of $($TrustDirection.ToString())."}

                $LocalForest.CreateTrustRelationship($RemoteForest, $TrustDirection)

                Write-Host "Trust creation completed, verifying trust."
                if ($EnableLogging) { Write-Log "Trust creation completed, verifying trust."}

                $LocalForest.VerifyTrustRelationship($RemoteForest, $TrustDirection)

                Write-Host "Trust creation verified."
                if ($EnableLogging) { Write-Log "Trust creation verified."}

                try {
                    if ($EnableLogging) { Write-Log "Setting Forest Trust selective authentication."}
                    Set-ADForestTrustSelectiveAuthentication -TrustDirection $TrustDirection -LocalForest $LocalForest -RemoteForest $RemoteForest -SelectiveAuthenticationEnabled $true -EnableLogging
                }
                catch [Exception] {
                    Write-Warning $_.Exception.Message
                    if ($EnableLogging) { Write-Log $_ }
                    $Continue = $false
                }

                Write-Host "Setting SID Filtering."
                if ($EnableLogging) { Write-Log "Setting SID Filtering." }

                switch ($TrustDirection) {
                    #Inbound trust means that the remote forest trusts the local forest
                    ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Inbound) {
                        Write-Host "Inbound trust, setting SID filtering on remote forest."
                        if ($EnableLogging) { Write-Log "Inbound trust, setting SID filtering on remote forest."}
                        
                        Set-ADForestTrustSIDFiltering -TrustingForest $RemoteForest -TrustedForest $LocalForest.Name -SidFilteringEnabled $SidFilteringEnabled -EnableLogging:$EnableLogging
                            
                        Write-Host "Completed enabling SID filtering."
                        if ($EnableLogging) { Write-Log "Completed enabling SID filtering."}
                        break
                    }
                    #Outbound trust means that the local forest trusts the remote forest
                    ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Outbound) {
                        Write-Host "Outbound trust, setting SID filtering on local forest."
                        if ($EnableLogging) { Write-Log "Outboud trust, setting SID filtering on local forest."}

                        Set-ADForestTrustSIDFiltering -TrustingForest $LocalForest -TrustedForest $RemoteForest.Name -SidFilteringEnabled $SidFilteringEnabled -EnableLogging:$EnableLogging
                        
                        Write-Host "Completed enabling SID filtering."
                        if ($EnableLogging) { Write-Log "Completed enabling SID filtering."}
                        break
                    }
                    ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Bidirectional) {
                        Write-Host "Bidirectional trust, setting SID filtering on both forests."
                        if ($EnableLogging) { Write-Log "Bidirectional trust, setting SID filtering on both forests."}

                        Set-ADForestTrustSIDFiltering -TrustingForest $RemoteForest -TrustedForest $LocalForest.Name -SidFilteringEnabled $SidFilteringEnabled -EnableLogging:$EnableLogging
                        Set-ADForestTrustSIDFiltering -TrustingForest $LocalForest -TrustedForest $RemoteForest.Name -SidFilteringEnabled $SidFilteringEnabled -EnableLogging:$EnableLogging
                            
                        Write-Host "Completed enabling SID filtering."
                        if ($EnableLogging) { Write-Log "Completed enabling SID filtering."}
                        break
                    }
                    default {
                        if ($EnableLogging) { Write-Log "Could not determine trust direction."}
                        throw "Could not determine trust direction."
                    }
                }

                if ($EnableLogging) { Write-Log "Remote domain supports Kerberos AES encryption: $TrustingDomainSupportsKerberosAESEncryption"}

                if ($TrustingDomainSupportsKerberosAESEncryption) {                
                    try
                    {
                        Write-Host "Setting Kerberos AES support."
                        if ($EnableLogging) { Write-Log "Setting Kerberos AES support."}                        
                        Set-ADForestTrustKerberosSupport -TrustDirection $TrustDirection -RemoteForest $RemoteForest.Name -LocalForest $LocalForest.Name -RemoteCredential $RemoteForestCredential -EnableLogging                            
                    }
                    catch [Exception] {
                        Write-Warning $_.Exception.Message
                        if ($EnableLogging) { Write-Log $_ }
                        $Continue = $false
                    }
                }            
            }
            catch [Exception] {
                Write-Warning $_.Exception.Message
                if ($EnableLogging) { Write-Log $_ }
                $Continue = $false
            }
        }
    }

    End {
        if (!$Continue) {
            $CommandText = Get-CommandText -Command ($PSCmdlet.MyInvocation.MyCommand.Name) -Parameters $PSBoundParameters
            Write-Warning "A step failed in creating the trust and its settings. Review the error and try again."
            Write-Host "Rerun this command: $CommandText"

            if ($EnableLogging) { Write-Log "A step failed in creating the trust and its settings. Review the error and try again." }
            if ($EnableLogging) { Write-Log "Rerun this command: $CommandText" }
        }
    }
}

Function Set-ADForestTrustSelectiveAuthentication {
    <#
        .SYNOPSIS
            The cmdlet sets up selective authentication for a forest trust.
 
        .DESCRIPTION
            The cmdlet enables or disables selective authentication for a forest trust between the specified forests.
 
        .PARAMETER TrustDirection
            The direction of the trust that should use selective authentication.
 
        .PARAMETER LocalForest
            The forest object for the local forest. This defaults to the forest of the computer where the cmdlet is being run.
 
        .PARAMETER RemoteForest
            The forest object for the remote forest.
 
        .PARAMETER LocalForestName
            The name of the local forest where the cmdlet is being run. This defaults to the forest root domain of the computer running the cmdlet.
 
        .PARAMETER RemoteForestName
            The name of the remote forest where the trust will connect.
 
        .PARAMETER LocalForestCredential
            The credential to use to setup the local side of the trust. The credential should have Enterprise Admin rights and defaults to the user running the cmdlet.
 
        .PARAMETER RemoteForestCredential
            The credential to use to setup the remote side of the trust. The credential should have Enterprise Admin rights in the remote forest.
 
        .PARAMETER SelectiveAuthenticationEnabled
            Specify whether selective authentication should be enabled or not. This defaults to false.
 
        .PARAMETER EnableLogging
            Specifies whether to use the module logging function when the cmdlet is run.
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-ADForestTrustSelectiveAuthentication -RemoteForestName "contoso.com" -RemoteForestCredential (Get-Credential) -SelectiveAuthenticationEnabled $true -TrustDirection ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Inbound)
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [System.DirectoryServices.ActiveDirectory.TrustDirection]$TrustDirection,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName="Objects")]
        [System.DirectoryServices.ActiveDirectory.Forest]$LocalForest = $null,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true,ParameterSetName="Objects",Mandatory=$true)]
        [System.DirectoryServices.ActiveDirectory.Forest]$RemoteForest,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName="Names")]
        [string]$LocalForestName = [System.String]::Empty,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true,ParameterSetName="Names",Mandatory=$true)]
        [string]$RemoteForestName,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true,ParameterSetName="Names")]
        [PSCredential]$LocalForestCredential = [System.Management.Automation.PSCredential]::Empty,
        [Parameter(Position=4,ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName="Names")]
        [PSCredential]$RemoteForestCredential,
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [bool]$SelectiveAuthenticationEnabled = $false,
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableLogging
    )

    Begin {

        switch ($PSCmdlet.ParameterSetName) {
            "Objects" {
                if ($LocalForest -eq $null) {
                    $LocalForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
                }
                break
            }
            "Names" {
                if ([System.String]::IsNullOrEmpty($LocalForestName)) {
                    $LocalForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
                }
                else {
                    try {
                        if ($LocalForestCredential -ne $null -and $LocalForestCredential -ne [PSCredential]::Empty) {
                            [System.DirectoryServices.ActiveDirectory.DirectoryContext]$LocalForestContext = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest, $LocalForestName, $LocalForestCredential.UserName, (Convert-SecureStringToString -SecureString $LocalForestCredential.Password))
                        }
                        else {
                            [System.DirectoryServices.ActiveDirectory.DirectoryContext]$LocalForestContext = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest, $LocalForestName)
                        }
                        [System.DirectoryServices.ActiveDirectory.Forest]$LocalForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($LocalForestContext)
                    }
                    catch [Exception] {
                        Write-Warning -Message $_.Exception.Message
                        if ($EnableLogging) { Write-Log $_ }
                        throw $_.Exception
                    }
                }

                try {
                    [System.DirectoryServices.ActiveDirectory.DirectoryContext]$RemoteForestContext = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest, $RemoteForestName, $RemoteForestCredential.UserName, (Convert-SecureStringToString -SecureString $RemoteForestCredential.Password))
                    [System.DirectoryServices.ActiveDirectory.Forest]$RemoteForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($RemoteForestContext)
                }
                catch [Exception] {
                    Write-Warning -Message $_.Exception.Message
                    if ($EnableLogging) { Write-Log $_ }
                    throw $_.Exception
                }
                break
            }
            default {
                throw "Parameter set for Set-ADForestTrustSelectiveAuthentication could not be determined."
            }
        }
    }

    Process {
        Write-Host "Setting selective authentication."
        if ($EnableLogging) { Write-Log "Setting selective authentication."}

        try {
            switch ($TrustDirection) {
                #Inbound trust means that the remote forest trusts the local forest
                ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Inbound) {
                    $RemoteForest.SetSelectiveAuthenticationStatus($LocalForest.Name, $SelectiveAuthenticationEnabled)
                    Write-Host "Completed setting selective authentication."
                    if ($EnableLogging) { Write-Log "Completed setting selective authentication."}
                    break
                }
                #Outbound trust means that the local forest trusts the remote forest
                ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Outbound) {
                    $LocalForest.SetSelectiveAuthenticationStatus($RemoteForest.Name, $SelectiveAuthenticationEnabled)
                    Write-Host "Completed setting selective authentication."
                    if ($EnableLogging) { Write-Log "Completed setting selective authentication."}
                    break
                }
                ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Bidirectional) {
                    $LocalForest.SetSelectiveAuthenticationStatus($RemoteForest.Name, $SelectiveAuthenticationEnabled)
                    $RemoteForest.SetSelectiveAuthenticationStatus($LocalForest.Name, $SelectiveAuthenticationEnabled)
                    Write-Host "Completed setting selective authentication."
                    if ($EnableLogging) { Write-Log "Completed setting selective authentication."}
                    break
                }
                default {
                    if ($EnableLogging) { Write-Log "Could not determine trust direction."}
                    throw "Could not determine trust direction."
                }
            }
        }
        catch [Exception] {
            if ($EnableLogging) { Write-Log $_ }
            throw $_.Exception
        }
    }

    End {

    }
}

Function Set-ADForestTrustKerberosSupport {
    <#
        .SYNOPSIS
            The cmdlet enables Kerberos AES 128 and 256 encryption in the forest.
 
        .DESCRIPTION
            The cmdlet uses ksetup.exe to enables AES 128 and 256 encryption in the forest specified by the trust direction. An inbound trust enables encryption in the local forest, outbound in the remote forest, and bidirectional
            in both forests.
 
        .PARAMETER TrustDirection
            The direction of the forest trust in relation to the server the cmdlet is being run on. This can be Inbound, Outbound, or Bidirectional.
 
        .PARAMETER LocalForest
            The name of the local forest where the cmdlet is being run. This defaults to the forest root domain of the computer running the cmdlet.
 
        .PARAMETER RemoteForest
            The name of the remote forest.
             
        .PARAMETER RemoteCredential
            The credential used to run Invoke-Command on the remote server that is selected via DNS resolution of the domain name. This credential needs access to the remote domain controller and to be able to run ksetup.exe in the remote forest.
 
        .PARAMETER PassThru
            Returns the new or modified object. By default (i.e. if -PassThru is not specified), this cmdlet does not generate any output.
     
        .PARAMETER EnableLogging
            Specifies whether to use the module's logging function when the cmdlet is run.
 
        .INPUTS
            None
         
        .OUTPUTS
            None or Microsoft.ActiveDirectory.Management.ADObject
 
        .EXAMPLE
            Set-ADForestTrustKerberosSupport -RemoteForest "contoso.com" -TrustDirection ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Inbound) -RemoteCredential (Get-Credential)
 
            Enables AES encryption in the contoso.com domain.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [System.DirectoryServices.ActiveDirectory.TrustDirection]$TrustDirection,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [string]$RemoteForest,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [string]$LocalForest = [System.String]::Empty,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [PSCredential]$RemoteCredential,
        [Parameter(Position=4,ValueFromPipelineByPropertyName=$true)]
        [switch]$PassThru,
        [Parameter(Position=5,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableLogging
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($LocalForest)) {
            $LocalForest = (Get-ADForest -Current LocalComputer).Name
        }
    }

    Process {
        switch ($TrustDirection) {
            #Inbound trust means that the remote forest trusts the local forest
            ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Inbound) {
                $Process = Start-Process -FilePath "$env:SYSTEMROOT\system32\ksetup.exe" -ArgumentList @("/setenctypeattr",$RemoteForest,"AES128-CTS-HMAC-SHA1-96","AES256-CTS-HMAC-SHA1-96") -NoNewWindow
                
                Write-Host "Configured Kerberos AES encryption setting."
                if ($EnableLogging) { Write-Log "Configured Kerberos AES encryption setting." }

                if ($PassThru) {
                    Write-Output (Get-ADObject -Filter {(objectClass -eq "trustedDomain") -and (name -eq $RemoteForest)} -Properties "msDS-SupportedEncryptionTypes") 
                }

                break
            }
            #Outbound trust means that the local forest trusts the remote forest
            ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Outbound) {
                $Trust = Invoke-Command -ComputerName $RemoteForest.Name -ScriptBlock {
                    $Domain = $args[0]
                    $Proc = Start-Process -FilePath "$env:SYSTEMROOT\system32\ksetup.exe" -ArgumentList @("/setenctypeattr",$Domain,"AES128-CTS-HMAC-SHA1-96","AES256-CTS-HMAC-SHA1-96") -NoNewWindow
                    Get-ADObject -Filter {(objectClass -eq "trustedDomain") -and (name -eq $Domain)} -Properties "msDS-SupportedEncryptionTypes" 
                } -ArgumentList ($LocalForest) -Credential $RemoteCredential
                                
                Write-Host "Configured Kerberos AES encryption setting."
                if ($EnableLogging) { Write-Log "Configured Kerberos AES encryption setting." }

                if ($PassThru) {
                    Write-Output $Trust
                }

                break
            }
            ([System.DirectoryServices.ActiveDirectory.TrustDirection]::Bidirectional) {
                $Process = Start-Process -FilePath "$env:SYSTEMROOT\system32\ksetup.exe" -ArgumentList @("/setenctypeattr",$RemoteForest,"AES128-CTS-HMAC-SHA1-96","AES256-CTS-HMAC-SHA1-96") -NoNewWindow
                $Trust = Invoke-Command -ComputerName $RemoteForest.Name -ScriptBlock {
                    $Domain = $args[0]
                    $Proc = Start-Process -FilePath "$env:SYSTEMROOT\system32\ksetup.exe" -ArgumentList @("/setenctypeattr",$Domain,"AES128-CTS-HMAC-SHA1-96","AES256-CTS-HMAC-SHA1-96") -NoNewWindow
                    Get-ADObject -Filter {(objectClass -eq "trustedDomain") -and (name -eq $Domain)} -Properties "msDS-SupportedEncryptionTypes" 
                } -ArgumentList ($LocalForest)

                Write-Host "Configured Kerberos AES encryption setting."
                if ($EnableLogging) { Write-Log "Configured Kerberos AES encryption setting." }
                
                if ($PassThru) {
                    Write-Output (Get-ADObject -Filter {(objectClass -eq "trustedDomain") -and (name -eq $RemoteForest)} -Properties "msDS-SupportedEncryptionTypes") 
                    Write-Output $Trust
                }

                break
            }
            default {
                Write-Warning "Could not determine trust direction."
                if ($EnableLogging) { Write-Log "Could not determine trust direction."}
            }
        }
    }

    End {

    }
}

Function Get-ADForestTrustRelationship {
    <#
        .SYNOPSIS
            The cmdlet gets the trust relationship information about the local and remote forest.
 
        .DESCRIPTION
            The cmdlet gets the trust relationship information about the local and remote forest. If no trust exists, the cmdlet returns null.
 
        .PARAMETER TargetForestName
            The name of the remote forest to get the trust information about.
 
        .PARAMETER CurrentForest
            The forest object to use to query about the trust information. This defaults to the current forest.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            Null or System.DirectoryServices.ActiveDirectory.ForestTrustRelationshipInformation
 
        .EXAMPLE
            Get-ADForestTrustRelationship -TargetForestName "contoso.com"
 
            Gets trust relationship information about contoso.com and the local forest.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$TargetForestName,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [System.DirectoryServices.ActiveDirectory.Forest]$CurrentForest = $null
    )

    Begin {
        if ($CurrentForest -eq $null) {
            $CurrentForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
        }
    }

    Process {
        try {
            Write-Output $CurrentForest.GetTrustRelationship($TargetForestName)
        }
        catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
            Write-Warning $_.Exception.Message
            Write-Output $null
        }
    }

    End {}
}

Function Set-ADForestTrustSIDFiltering {
    <#
        .SYNOPSIS
            The cmdlet sets SID filtering between two forests.
 
        .DESCRIPTION
            The cmdlet enables or disables SID filtering between two forests with a forest trust.
 
        .PARAMETER TrustingForest
            The forest object that trusts the other forest. The trust direction would be outbound or bidirectional for this forest. This defaults to the current forest.
 
        .PARAMETER TrustedForest
            The forest that is trusted by the other forest. The trust direction would be inbound or bidirectional for this forest.
 
        .PARAMETER SidFilteringEnabled
            Specify whether SID filtering should be enabled. This defaults to true.
 
        .PARAMETER EnableLogging
            Specify whether the module's logging function should be used when running the cmdlet.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Set-ADForestTrustSIDFiltering -TrustedForest "contoso.com"
 
            The local forest, admin.local, that trusts contoso.com, has SID filtering enabled on the trust.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [System.DirectoryServices.ActiveDirectory.Forest]$TrustingForest = $null,
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$TrustedForest,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [bool]$SidFilteringEnabled = $true,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableLogging
    )

    Begin {
        if ($TrustingForest -eq $null) {
            $TrustingForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
        }
    }

    Process {
        try {
            Write-Host "Setting SID Filtering for $($TrustingForest.Name) to $SidFilteringEnabled."
            if ($EnableLogging) { Write-Log "Setting SID Filtering for $($TrustingForest.Name) to $SidFilteringEnabled." }
            
            $TrustingForest.SetSidFilteringStatus($TrustedForest, $SidFilteringEnabled)

            Write-Host "Successfully set SID Filtering to $SidFilteringEnabled."
            if ($EnableLogging) { Write-Log "Successfully set SID Filtering to $SidFilteringEnabled."}

        }
        catch [Exception] {
            Write-Warning $_.Exception.Message
            if ($EnableLogging) { Write-Log $_ }
        }
    }

    End {

    }
}

#endregion

#region Scheduled Task Functions

Function New-SaveEncryptedPasswordTask {
    <#
        .SYNOPSIS
            The cmdlet runs a scheduled task as a specified account to save a password encrypted with the principal's credentials.
 
        .DESCRIPTION
            The cmdlet runs a scheduled task as a specified account to save a password encrypted with the principal's credentials. The scheduled task is deleted after the task is complete.
 
        .PARAMETER Password
            The password to encrypt and save.
 
        .PARAMETER FilePath
            The location to save the password file. This defaults to "$env:ALLUSERSPROFILE\Microsoft\ADPassword.txt".
 
        .PARAMETER Principal
            The principal that the scheduled task will run as. This defaults to SYSTEM.
 
        .PARAMETER Timeout
            The timeout for the scheduled task. This defaults to 10 minutes.
 
        .INPUTS
            System.Security.SecureString
         
        .OUTPUTS
            System.String
            The file path of the encrypted password.
 
        .EXAMPLE
            New-SaveEncryptedPasswordTask
 
            The cmdlet will prompt the user to enter the password, and then run a scheduled task as SYSTEM to encrypt the password.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [SecureString]$Password = $null,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$FilePath = "$env:ALLUSERSPROFILE\Microsoft\ADPassword.txt",
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [Microsoft.Management.Infrastructure.CimInstance]$Principal = $null,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true)]
        [System.Timespan]$Timeout = [System.Timespan]::FromSeconds(600)
    )

    Begin {
        if ($FilePath -eq $null -or $FilePath -eq [System.String]::Empty) {
            throw [System.ArgumentNullException]("The specified file path was null or empty.")
        }

        while($Password -eq $null) {
            $Password = Read-Host -AsSecureString -Prompt "Enter the password to encrypt"
        }

        $TempPass = Convert-SecureStringToString -SecureString $Password
        $Command = "try {`$EncryptedPass = New-EncryptedPassword -Password $TempPass; Set-Content -Path $FilePath -Value `$EncryptedPass -Force } catch [Exception] { Write-Log `$_ }"
        $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command)
        $EncodedCommand = [Convert]::ToBase64String($Bytes)

        $TempPass = $null 
    }

    Process {
        Write-Host "Creating scheduled task to save password."

        $STParams = "-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -EncodedCommand $EncodedCommand"
    
        $STSource =  "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe"
        $STAction = New-ScheduledTaskAction -Execute $STSource -Argument $STParams
        
        if ($Principal -eq $null) {
            $STPrincipal = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest
        }
        else {
            $STPrincipal = $Principal
        }

        $STSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -DontStopIfGoingOnBatteries -DontStopOnIdleEnd
                        
        if ((Get-ScheduledTask -TaskName "SavePassword" -ErrorAction SilentlyContinue) -ne $null) {
            Unregister-ScheduledTask -TaskName "SavePassword" -Confirm:$false
        }
        
        Write-Host "Registering scheduled task."              
        $ScheduledTask = Register-ScheduledTask -TaskName "SavePassword" -Action $STAction -Principal $STPrincipal -Settings $STSettings -ErrorAction Stop

        Write-Host "Executing scheduled task."
        Start-ScheduledTask -TaskName "SavePassword"

        Write-Host "Waiting for password file to be created."
        $Counter = 0
        while (!(Test-Path -Path $FilePath)) {
            Start-Sleep -Seconds 1
            $Counter++

            if ($Counter -gt $Timeout.TotalSeconds) {
                Write-Warning "Password file failed to be created before timeout."
                throw "Password file failed to be created before timeout."
            }
        }

        Write-Host "Password file successfully created."
    }

    End {
        if ((Get-ScheduledTask -TaskName "SavePassword" -ErrorAction SilentlyContinue) -ne $null) {
            Write-Host "Removing scheduled task."
            Unregister-ScheduledTask -TaskName "SavePassword" -Confirm:$false          
        }

        Write-Output $FilePath
    }                 
}

Function New-InstallSecureADForestScheduledTask {
    <#
        .SYNOPSIS
            The cmdlet creates a scheduled task to begin the Install-SecureADForest cmdlet after the next reboot.
 
        .DESCRIPTION
            The cmdlet creates a scheduled task to begin the Install-SecureADForest after the next reboot that will run as the specified principal. In order to both read the encrypted password and run the scheduled task with
            the appropriate permissions, the password should be encrypted with SYSTEM credentials and the scheduled task should be run as SYSTEM. This is the default configuration.
 
        .PARAMETER ConfigFilePath
            The path to the config file that Install-SecureADForest will use to execute.
 
        .PARAMETER PasswordFilePath
            The path to the password file containing the DSRM safe mode password.
 
        .PARAMETER Principal
            The principal that the scheduled task will run as. This defaults to SYSTEM.
 
        .INPUTS
            None
         
        .OUTPUTS
            Microsoft.Management.Infrastructure.CimInstance#MSFT_ScheduledTask
 
        .EXAMPLE
            New-InstallSecureADForestScheduledTask -ConfigFilePath "$PSScriptRoot\config.json" -PasswordFilePath "$env:ALLUSERSPROFILE\Microsoft\ADPassword.txt"
 
            Creates the scheduled task to run Install-SecureADForest as SYSTEM.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$ConfigFilePath,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$PasswordFilePath,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [Microsoft.Management.Infrastructure.CimInstance]$Principal = $null
    )

    Begin {
         if ((Get-ScheduledTask -TaskName $script:InstallSecureADForestTask -ErrorAction SilentlyContinue) -ne $null) {
            Unregister-ScheduledTask -TaskName $script:InstallSecureADForestTask -Confirm:$false
         }
    }

    Process {
        $Command = "try {`$SafeModePassword = Get-EncryptedPassword -FilePath `"$PasswordFilePath`"; Remove-Item -Path `"$PasswordFilePath`" -Force -Confirm:`$false; Install-SecureADForest -ConfigFilePath `"$ConfigFilePath`" -SafeModePassword `$SafeModePassword} catch [Exception] {Write-Log `$_}"
        $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command)
        $EncodedCommand = [Convert]::ToBase64String($Bytes)
        
        $STParams = "-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -EncodedCommand $EncodedCommand"
        $STSource =  "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe"
        $STAction = New-ScheduledTaskAction -Execute $STSource -Argument $STParams
        
        if ($Principal -eq $null) {
            $STPrincipal = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest
        }
        else {
            $STPrincipal = $Principal
        }

        $STSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew
        $ScheduledTask = Register-ScheduledTask -TaskName $script:InstallSecureADForestTask -Action $STAction -Principal $STPrincipal -Trigger (New-ScheduledTaskTrigger -AtStartup -RandomDelay ([System.Timespan]::FromSeconds(30))) -Settings $STSettings -ErrorAction Stop
    }

    End {
        Write-Output $ScheduledTask
    }
}

Function New-FinishSecureADForestInstallationScheduledTask {
    <#
        .SYNOPSIS
            The cmdlet creates a scheduled task to begin the Set-ADForestSecurityConfiguration cmdlet after the next reboot.
 
        .DESCRIPTION
            The cmdlet creates a scheduled task to begin the Set-ADForestSecurityConfiguration cmdlet after the next reboot that will run as the specified principal.
 
        .PARAMETER ConfigFilePath
            The path to the config file that Set-ADForestSecurityConfiguration will use to execute.
 
        .PARAMETER Principal
            The principal that the scheduled task will run as. This defaults to SYSTEM.
 
        .INPUTS
            None
         
        .OUTPUTS
            Microsoft.Management.Infrastructure.CimInstance#MSFT_ScheduledTask
 
        .EXAMPLE
            New-FinishSecureADForestInstallationScheduledTask -ConfigFilePath "$PSScriptRoot\config.json"
 
            Creates the scheduled task to run Set-ADForestSecurityConfiguration as SYSTEM.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$ConfigFilePath,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [Microsoft.Management.Infrastructure.CimInstance]$Principal = $null
    )

    Begin {
        if ((Get-ScheduledTask -TaskName $script:FinishSecureADForestTask -ErrorAction SilentlyContinue) -ne $null) {
                Unregister-ScheduledTask -TaskName $script:FinishSecureADForestTask -Confirm:$false
        }
    }

    Process {
        $Command = "try {Set-ADForestSecurityConfiguration -ConfigFilePath `"$ConfigFilePath`"} catch [Exception] {Write-Log `$_}"                            
        $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command)
        $EncodedCommand = [Convert]::ToBase64String($Bytes)
        
        $STParams = "-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -EncodedCommand $EncodedCommand"
        $STSource =  "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe"
        $STAction = New-ScheduledTaskAction -Execute $STSource -Argument $STParams

        if ($Principal -eq $null) {
            $STPrincipal = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest
        }
        else {
            $STPrincipal = $Principal
        }

        $STSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew
                               
        $ScheduledTask = Register-ScheduledTask -TaskName $script:FinishSecureADForestTask -Action $STAction -Principal $STPrincipal -Trigger (New-ScheduledTaskTrigger -AtStartup -RandomDelay ([System.Timespan]::FromSeconds(30))) -Settings $STSettings -ErrorAction Stop 
    }
    
    End {
        Write-Output $ScheduledTask
    }
}

Function Enable-TaskSchedulerHistory {
    <#
        .SYNOPSIS
            The cmdlet enables the task scheduler history logs.
 
        .DESCRIPTION
            The cmdlet enables the task scheduler history logs.
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Enable-TaskSchedulerHistory
 
            Enables the task scheduler history.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param()
    Begin {}

    Process {
        $LogName = 'Microsoft-Windows-TaskScheduler/Operational'
        $EventLog = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $LogName
        $EventLog.IsEnabled = $true
        $EventLog.SaveChanges()
    }

    End{}
}

#endregion

#region Utility Functions

Function Test-RpcAvailability {
    <#
        .SYNOPSIS
            The cmdlet tests whether the RPCSS service is available on a computer.
 
        .DESCRIPTION
            The cmdlet tests whether the RPCSS service is available on a computer by running a Get-Service against the provided computer name.
 
        .PARAMETER ComputerName
            The computer to check the service availability on.
 
        .PARAMETER EnableLogging
            Specify whether to use the module's logging function.
 
        .PARAMETER Wait
            If the initial request returns null, specify whether to wait until the timeout to continue testing for the service's availability.
 
        .PARAMETER Timeout
            If the Wait parameter is used, the timeout as a multiple of 10 seconds to keep trying before failing. This defaults to 180, which is 1800 seconds, or 30 minutes.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            System.Boolean
 
        .EXAMPLE
            Test-RpcAvailability -ComputerName "AdminDC" -Wait
 
            Tests for the availability of the RPCSS service and waits up to 30 minutes for it to become available.
 
        .NOTES
            None
    #>

    [CmdletBinding()] 
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$ComputerName = [System.String]::Empty,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnableLogging,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [switch]$Wait,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true)]
        [int]$Timeout = 180
    )

    Begin {
        if ([System.String]::IsNullOrEmpty($ComputerName)) {
            $ComputerName = $env:COMPUTERNAME
        }
    }

    Process {
        Write-Host "Testing for RPC availability on $ComputerName."
        if ($EnableLogging) { Write-Log "Testing for RPC availability on $ComputerName." }

        $RpcSs = $RpcSs = Get-Service -ComputerName $ComputerName -Name RpcSs -ErrorAction SilentlyContinue

        if ($RpcSs -eq $null) {
            if ($Wait) {
                $Counter = 0
            
                while ($RpcSs -eq $null) {
                    if ($Counter -gt $Timeout) {
                        if ($EnableLogging) { Write-Log "Timeout waiting for RpcSs service to be loaded. Could not create local stub zone." }
                        Write-Output $false
                    }
                
                    Start-Sleep -Seconds 10
                    $Counter++
                    try {
                        $RpcSs = Get-Service -ComputerName $ComputerName -Name RpcSs -ErrorAction SilentlyContinue

                        Write-Host ("Current RpcSs status: RpcSs = null : " + ($RpcSs -eq $null))
                        if ($EnableLogging) { Write-Log ("Current RpcSs status: RpcSs = null : " + ($RpcSs -eq $null)) }
                    }
                    catch [Exception] {
                        if ($EnableLogging) { Write-Log ("Iteration $Counter " + $_.Exception.Message) }
                    }
                }
            }
            else {
                if ($EnableLogging) { Write-Log "RpcSs service is not available." }
                Write-Output $false
            }
        }
        
        if ($EnableLogging) { Write-Log ("RpcSs service found: " + ($RpcSs -ne $null) + ".")}
        if ($EnableLogging) { Write-Log "Connected to $($RpcSs.MachineName)." }

        $ComputerName = $RpcSs.MachineName

        $RpcEptMapper = Get-Service -ComputerName $ComputerName -Name RpcEptMapper
        
        if ($EnableLogging) { Write-Log "Current RpcSs status: $($RpcSs.Status)." }
        if ($EnableLogging) { Write-Log "Current RpcEptMapper status: $($RpcEptMapper.Status)." }

        
        if ($RpcSs.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running -and $RpcEptMapper.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running) {
            if ($Wait) {
                $Counter = 0
            
                while ($RpcSs.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running -and $RpcEptMapper.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running) {
                    if ($Counter -gt 180) {
                        if ($EnableLogging) { Write-Log "Timeout waiting for RpcSs and RpcEptMapper to start. Could not create local stub zone." }
                        Write-Output $false
                    }

                    Start-Sleep -Seconds 10
                    $Counter++
                    $RpcSs = Get-Service -ComputerName $ComputerName -Name RpcSs
                    $RpcEptMapper = Get-Service -ComputerName $ComputerName -Name RpcEptMapper

                    Write-Host "Current RpcSs status: $($RpcSs.Status)."
                    Write-Host "Current RpcEptMapper status: $($RpcEptMapper.Status)."
                    if ($EnableLogging) { Write-Log "Current RpcSs status: $($RpcSs.Status)." }
                    if ($EnableLogging) { Write-Log "Current RpcEptMapper status: $($RpcEptMapper.Status)." }                
                }
            }
            else {
                if ($EnableLogging) { Write-Log "The RpcSs and RpcEptMapper services are not running." }
                Write-Output $false
            }
        }

        Write-Host "Rpc services are running, testing WMI connection via RPC."
        if ($EnableLogging) { Write-Log "Rpc services are running, testing WMI connection via RPC." }

        $Comp = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName -ErrorAction SilentlyContinue

        if ($Comp -ne $null) {
            if ($EnableLogging) { Write-Log "Successfully connected to WMI via RPC."}
            Write-Output $true
        }
        else {
            if ($EnableLogging) { Write-Log "Could not connect to WMI via RPC, cannot create Stub Zone." }
            Write-Output $false
        }                
    }

    End {

    }
}

Function New-EncryptedPassword {
    <#
        .SYNOPSIS
            The cmdlet creates a password encrypted with the calling user's credentials.
 
        .DESCRIPTION
            The cmdlet creates a password encrypted with the calling user's credentials via the Windows Data Protection API (DPAPI).
 
        .PARAMETER Password
            The plain text password to encrypt.
 
        .PARAMETER SecurePassword
            The secure string password to encrypt.
 
        .INPUTS
            System.String
 
            System.Security.SecureString
         
        .OUTPUTS
            System.String
 
        .EXAMPLE
            New-EncryptedPassword -Password "MySecurePassword"
 
            Encrypts the password with the calling user's credentials.
 
        .NOTES
            None
    #>

    [CmdletBinding(DefaultParameterSetName="SecureString")]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName="PlainText")]
        [string]$Password,
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName="SecureString")]
        [SecureString]$SecurePassword
    )

    Begin {
        switch ($PSCmdlet.ParameterSetName) {
            "PlainText" {
                Write-Host "Converting plaintext password to secure string."
                [SecureString]$SecurePass = ConvertTo-SecureString -String $Password -AsPlainText -Force
                break
            }
            "SecureString" {
                Write-Host "Password already a secure string."
                [SecureString]$SecurePass = $SecurePassword
                break
            }
            default {
                throw "Could not determine parameter set for Save-EncryptedPassword."
            }
        }
    }

    Process {
        Write-Host "Encrypting secure string password."
        Write-Output (ConvertFrom-SecureString -SecureString $SecurePass)
    }

    End {}
}

Function Get-EncryptedPassword {
    <#
        .SYNOPSIS
            The cmdlet unencrypts an encrypted string stored in a file.
 
        .DESCRIPTION
            The cmdlet unencrypts a string stored in a file using the calling user's credentials via the Windows Data Protection API (DPAPI).
 
        .PARAMETER FilePath
            The path to the file with the encrypted password.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            System.Security.SecureString
 
        .EXAMPLE
            Get-EncryptedPassword -FilePath "c:\password.txt"
 
            Unencrypts the password stored in the file with the calling user's credentials.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$FilePath
    )

    Begin {
        if (!(Test-Path -Path $FilePath)) {
            Write-Warning "The specified file does not exist."
            throw [System.IO.FileNotFoundException]("$FilePath not found.")
        }
    }

    Process {
        [SecureString]$Password = Get-Content -Path $FilePath | ConvertTo-SecureString
    }

    End {
        Write-Output $Password
    }
}

Function Convert-SecureStringToString {
    <#
        .SYNOPSIS
            The cmdlet converts a secure string to standard string.
 
        .DESCRIPTION
            The cmdlet converts a secure string to standard string.
 
        .PARAMETER SecureString
            The secure string to convert to a standard string
 
        .INPUTS
            System.Security.SecureString
         
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String "test" -AsPlainText -Force)
 
            Converts the secure string created from the text "test" back to plain text.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [SecureString]$SecureString
    )

    Begin {}

    Process {
        $Marshal = [System.Runtime.InteropServices.Marshal]   
        $Password = [System.String]::Empty
        try 
        {     
            $IntPtr = $Marshal::SecureStringToBSTR($SecureString)     
            $Password = $Marshal::PtrToStringAuto($IntPtr)   
        }   
        finally 
        {     
            if ($IntPtr) {       
                $Marshal::ZeroFreeBSTR($IntPtr)     
            }   
        }
    }

    End {
        Write-Output $Password
    }
}

Function Extract-ZipFile {
    <#
        .SYNOPSIS
            The cmdlet extracts the contents of a zip file to a specified destination.
 
        .DESCRIPTION
            The cmdlet extracts the contents of a zip file to a specified destination and optionally preserves the contents in the destination if they already exist.
 
        .PARAMETER Source
            The path to the zip file.
 
        .PARAMETER Destination
            The folder where the zip file should be extracted. The destination is created if it does not already exist.
 
        .PARAMETER NoOverwrite
            Specify if the contents in the destination should be preserved if they already exist.
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Extract-ZipFile -Source "c:\test.zip" -Destination "c:\test"
 
            Extracts the contents of test.zip to c:\test.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$Source,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [string]$Destination,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [switch]$NoOverwrite = $false
    )

    Begin {
        if (!(Test-Path -Path $Source)) {
            throw [System.IO.FileNotFoundException]("Source zip file not found.")
        }

        if (!(Test-Path -Path $Destination)) {
            Write-Log "Zip extract destination $Destination does not exist, creating it."
            try {
                New-Item -Path $Destination -ItemType Directory | Out-Null

                $Counter = 0

                while (!(Test-Path -Path $Destination)) {
                    Start-Sleep -Seconds 1
                    $Counter++

                    if ($Counter -gt 60) {
                        throw "Timeout error waiting for the zip extraction destination $Destination to be created."
                    }
                }
            }
            catch [Exception] {
                Write-Log $_
                throw $_.Exception
            }
        }
        else {
            if (![System.IO.Directory]::Exists($Destination)) {
                throw [System.IO.DirectoryNotFoundException]("The destination is a file, not a directory.")
            }
        }

        Add-Type -AssemblyName System.IO.Compression.FileSystem
    }

    Process {
        
        if (!$NoOverwrite) {
            [System.IO.Compression.ZipArchive]$ZipArchive = [System.IO.Compression.ZipFile]::OpenRead($Source)

            try
            {
                foreach ($ZipArchiveEntry in $ZipArchive.Entries) {
                    $FullPath = [System.IO.Path]::Combine($Destination, $ZipArchiveEntry.FullName)

                    #Test to see if the archive entry is a directory
                    #Directories' name attribute is empty,
                    if ([System.String]::IsNullOrEmpty($ZipArchiveEntry.Name) -or $ZipArchiveEntry.FullName.Contains("/")) {
                        $Temp = [System.IO.Path]::Combine($Destination, $ZipArchiveEntry.FullName.Substring(0, $ZipArchiveEntry.FullName.LastIndexOf("/")))
                        $Temp = $Temp.Replace("/","\")
                        if (![System.IO.Directory]::Exists($Temp)) {
                            try {
                                New-Item -Path $Temp -ItemType Directory | Out-Null

                                $Counter = 0
                                while (!(Test-Path -Path $Temp)) {
                                    Start-Sleep -Seconds 1
                                    $Counter++

                                    if ($Counter -gt 60) {
                                        throw "Timeout waiting for directory creation $Temp"
                                    }
                                }
                            }
                            catch [Exception] {
                                Write-Log $_
                            }
                        }
                    }

                    if (![System.String]::IsNullOrEmpty($ZipArchiveEntry.Name)) {
                        try
                        {
                            $FullPath = $FullPath.Replace("/","\")

                            [System.IO.Compression.ZipFileExtensions]::ExtractToFile($ZipArchiveEntry, $FullPath, $true)

                            $Counter = 0
                            
                            while(!(Test-Path -Path $FullPath)) {
                                Start-Sleep -Seconds 1
                                $Counter++

                                if ($Counter -gt 60) {
                                    Write-Log "Timeout waiting for zip extraction of $FullPath"
                                    break
                                }
                            }
                        }
                        catch [Exception] {
                            Write-Log $_
                        }
                    }
                }
            }
            finally {
                $ZipArchive.Dispose()
            }
        }
        else {
            [System.IO.Compression.ZipFile]::ExtractToDirectory($Source, $Destination)
        }
    }

    End {
        
    }
}

Function Write-Log {
    <#
        .SYNOPSIS
            The cmdlet writes messages and error records in a standard format to a log file.
 
        .DESCRIPTION
            The cmdlet writes messages and error records in a standard format to a log file.
 
        .PARAMETER Message
            The raw message to write to the log file.
 
        .PARAMETER ErrorRecord
            The error record produced from a try/catch in PowerShell to write to the log file.
 
        .PARAMETER Path
            The path to the log file. If it does not exist, the file is created.
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Write-Log -Message "An error message." -Path "c:\log.txt"
 
            Writes a message to c:\log.txt.
 
        .NOTES
            None
    #>

    [CmdletBinding(DefaultParameterSetName="String")]
    Param(
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName="String", Position=0)]
        [string]$Message,
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName="Exception", Position=0)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$Path = [System.String]::Empty
    )

    Begin {
        switch ($PSCmdlet.ParameterSetName) {
            "String" {
                break
            }
            "Exception" {
                $Message = "`n"
                $Message += ("Exception: `n" + ($ErrorRecord.Exception | Select-Object -Property * | Format-List | Out-String) + "`n")
                $Message += ("Category: " + ($ErrorRecord.CategoryInfo.Category.ToString()) + "`n")
                $Message += ("Stack Trace: `n" + ($ErrorRecord.ScriptStackTrace | Format-List | Out-String) + "`n")
                $Message += ("Invocation Info: `n" + ($ErrorRecord.InvocationInfo | Format-List | Out-String))
                break
            }
            default {
                throw "Could not determine the parameter set name for Write-Log."
            }
        }

        if ([System.String]::IsNullOrEmpty($Path)) {
            if (![System.String]::IsNullOrEmpty($env:NewADForestLogPath)) {
                $Path = $env:NewADForestLogPath
            }
            else {
                $Path = $script:LogPath
            }
        }
    }

    Process {
        Add-Content -Path $Path -Value ((Get-Date).ToString() + " : " + $Message)
    }

    End {}
}

Function New-ConfigurationFile {
    <#
        .SYNOPSIS
            The cmdlet writes creates a new configuration json file from the inputted parameters of a cmdlet.
 
        .DESCRIPTION
            The cmdlet writes creates a new configuration json file from the inputted parameters of a cmdlet.
 
        .PARAMETER ParameterList
            The list of parameters to create the configuration file with.
 
        .PARAMETER SourceFilePath
            The path to the existing configuration file.
 
        .PARAMETER Destination
            The path to where the resulting configuration file is written.
 
        .INPUTS
            None
         
        .OUTPUTS
            System.Collections.Hashtable
            An object that contains the path to the configuration file and a JSON string of the content.
 
        .EXAMPLE
            $ParameterList = @{}
            $ParameterList.Add("Name", "John Smith")
            $Info = New-ConfigurationFile -ParameterList $ParameterList -Destination "c:\config.json"
 
            Creates a new configuration file at c:\config.json and returns an object with the file path and configuration content.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,ParameterSetName="Parameters",Position=0,ValueFromPipelineByPropertyName=$true)]
        [System.Collections.Hashtable]$ParameterList,
        [Parameter(Mandatory=$true,ParameterSetName="File",Position=0,ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$SourceFilePath,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [string]$Destination = [System.String]::Empty
    )

    Begin {
        $Values = @()

        if ([System.String]::IsNullOrEmpty($Destination)) {
            $Destination = [System.IO.Path]::Combine($PSScriptRoot, "config.json")
        }
     }

    Process {

        switch ($PSCmdlet.ParameterSetName) {
            "Parameters" {
                $Json = ConvertTo-Json -InputObject $ParameterList
        
                Set-Content -Value $Json -Path $Destination
                break
            }
            "File" {
                Copy-Item -Path $SourceFilePath -Destination $Destination
                
                $Counter = 0
                while (!(Test-Path -Path $Destination)) {
                    Start-Sleep -Seconds 1
                    $Counter++

                    if ($Counter -gt 120) {
                        throw "Timeout waiting for config file to be copied."
                    }
                }    
                $Json = Get-Content -Path $Destination -Raw
                break
            }
        }
    }

    End {
        Write-Output @{"FilePath"=$Destination;"Content"=$Json}
    }
}

Function Get-CommandText {
    <#
        .SYNOPSIS
            The cmdlet generates a string of the parameters and values that were used to run a cmdlet.
 
        .DESCRIPTION
            The cmdlet generates a string of the parameters and values that were used to run a cmdlet.
 
        .PARAMETER Command
            The command that was run.
 
        .PARAMETER Parameters
            The parameters that were specified when running the command.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-CommandText -Command "Get-Service"
 
            Recreates the string "Get-Service"
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$Command,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        [Object]$Parameters = $null
    )

    Begin {

    }

    Process {
        
        if ($Parameters -ne $null) {
            $Parameters.GetEnumerator() | ForEach-Object {
                $Param = $_.Key
                $Value = $_.Value

                if ($Value.GetType() -eq [System.String]) {
                    $Command += " -$Param `"$Value`""
                }
                else {
                    if ($Value.GetType() -eq [System.Boolean]) {
                        $Command += " -$Param `$$($Value.ToString().ToLower())"
                    }
                    else {
                        $Command += " -$Param $Value"
                    }
                }
            }
        }
    }

    End {
        Write-Output $Command
    }
}

Function New-RandomPassword {
    <#
        .SYNOPSIS
            The cmdlet generates a random string.
 
        .DESCRIPTION
            The cmdlet generates a random string with a specific length and complexity settings.
 
        .PARAMETER Length
            The length of the returned string, this defaults to 14.
 
        .PARAMETER SourceData
            The range of characters that can be used to generate the string. This defaults to
 
            for ($a=33; $a -le 126; $a++) {
                $SourceData += ,[char][byte]$a
            }
 
            which contains upper, lower, number, and special characters.
 
        .PARAMETER EnforceComplexity
            Specify to ensure the produced string has at least 2 upper, 2 lower, 2 number, and 2 special characters.
 
        .PARAMETER AsSecureString
            Specify to return the result as a secure string instead of a standard string.
 
        .INPUTS
            System.Int32
         
        .OUTPUTS
            System.String
 
            System.Security.SecureString
 
        .EXAMPLE
            $Pass = New-RandomPassword
 
            Generates a new random password.
 
        .NOTES
            None
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [int]$Length=14,
        [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
        $SourceData = $null,
        [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)]
        [switch]$EnforceComplexity,
        [Parameter(Position=3,ValueFromPipelineByPropertyName=$true)]
        [switch]$AsSecureString
    )

    Begin {
        $Password = [System.String]::Empty

        if ($SourceData -eq $null) {        
            for ($a=33; $a -le 126; $a++) {
                $SourceData += ,[char][byte]$a 
            }          
        }

        if ($EnforceComplexity) {
            if ($Length -lt 14) {
                $Length = 14
            }
        }
    }

    Process {

        if ($EnforceComplexity) {
            $Upper = 0
            $Lower = 0
            $Special = 0
            $Number = 0

            while ($Upper -lt 2 -or $Lower -lt 2 -or $Special -lt 2 -or $Number -lt 2) {
                $Upper = 0
                $Lower = 0
                $Special = 0
                $Number = 0

                $Password = ""

                for ($i=1; $i �le $Length; $i++) {
                    $Password += ($SourceData | Get-Random)
                }

                for ($i = 0; $i -lt $Password.Length; $i++) {
                    if ([System.Char]::IsUpper($Password[$i])) {
                        $Upper++
                    }
                    if ([System.Char]::IsLower($Password[$i])) {
                        $Lower++
                    }
                    if ([System.Char]::IsSymbol($Password[$i])) {
                        $Special++
                    }
                    if ([System.Char]::IsNumber($Password[$i])) {
                        $Number++
                    }
                }
            }
        }
        else {
            for ($i=1; $i �le $Length; $i++) {
                $Password += ($SourceData | Get-Random)
            }
        }
    }

    End {          
        if ($AsSecureString) {
            Write-Output (ConvertTo-SecureString -String $Password -AsPlainText -Force)
        }
        else {
            Write-Output $Password
        }
    }
}

#endregion

$script:TrustAttributes = @(
            [PSCustomObject]@{Key="0x00000001";Value="TRUST_ATTRIBUTE_NON_TRANSITIVE"},
            [PSCustomObject]@{Key="0x00000002";Value="TRUST_ATTRIBUTE_UPLEVEL_ONLY"},
            [PSCustomObject]@{Key="0x00000004";Value="TRUST_ATTRIBUTE_QUARANTINED_DOMAIN"},
            [PSCustomObject]@{Key="0x00000008";Value="TRUST_ATTRIBUTE_FOREST_TRANSITIVE"},
            [PSCustomObject]@{Key="0x00000010";Value="TRUST_ATTRIBUTE_CROSS_ORGANIZATION"}
            [PSCustomObject]@{Key="0x00000020";Value="TRUST_ATTRIBUTE_WITHIN_FOREST"},
            [PSCustomObject]@{Key="0x00000040";Value="TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL"},
            [PSCustomObject]@{Key="0x00000080";Value="TRUST_ATTRIBUTE_USES_RC4_ENCRYPTION"},
            [PSCustomObject]@{Key="0x00000200";Value="TRUST_ATTRIBUTE_CROSS_ORGANIZATION_NO_TGT_DELEGATION"},
            [PSCustomObject]@{Key="0x00000400";Value="TRUST_ATTRIBUTE_PIM_TRUST"}
        )