helpers/dry.ad.gpohelper/dry.ad.gpohelper.psm1

Using Namespace System.DirectoryServices.ActiveDirectory
#Using Module GPRegistryPolicyParser

class BaseSettings {
    [string]$ObjectType
}

class GroupPolicy : BaseSettings {
    [string]$Name
    [LinkTarget[]]$Linktargets
    [bool]$ComputerSettingsEnabled
    [bool]$UserSettingsEnabled
    [WMIFilter]$WMIFilter
    [string]$Permissions
    [PolicySettings]$PolicySettings
    [string]$gPCMachineExtensionNames
    [string]$gPCUserExtensionNames

    GroupPolicy () {
        $this.ObjectType = "GroupPolicy"
        $this.PolicySettings = [PolicySettings]::new()
    }

    GroupPolicy (
        [string]$Name
    ) {
        $this.Name = $Name
        $this.ObjectType = "GroupPolicy"
        $this.PolicySettings = [PolicySettings]::new()
    }
    
    # Get GPO from AD and save it to json-file all in one go
    GroupPolicy (
        [string]$Name,
        [bool]$GetLinks,
        [string]$FileName
    ) {
        $this.ObjectType = "GroupPolicy"
        $this.PolicySettings = [PolicySettings]::new()
        $this.GetPolicyFromAD($Name, $GetLinks)
        $this.WritePolicyToJson($FileName)
    }

    # Read json-file and import to AD in one operation
    GroupPolicy (
        [string]$FileName,
        [hashtable]$Replacements,
        [bool]$Overwrite,
        [bool]$Backup,
        [bool]$RemoveLink
    ) {
        $this.ObjectType = "GroupPolicy"
        $this.PolicySettings = [PolicySettings]::new()
        $this.GetPolicyFromJson($FileName, $Replacements, $false)
        $this.WritePolicyToAD($Overwrite, $Backup, $RemoveLink)
    }


    [void] hidden IncrementVersion(
        [string]$id,
        [string]$DomainController,
        [string]$DomainFQDN,
        [uint32]$IncrementBy
    ) {
        $Utils = [Utils]::new()
        $properties = @("gPCFileSysPath","versionNumber")
        $Retry = $true
        $Count = 0
        $gpContainer = $null
        do {
            $gpContainer = Get-ADObject -LDAPFilter "(&(CN={$id})(objectclass=groupPolicyContainer))" -properties $properties -Server $DomainController
            if ($gpContainer) {
                $Retry = $false
            } else {
                Start-Sleep -seconds 1
                $count++
                if ($count -gt 60) {
                    throw "Failed to get CN={$id} after 60 retries."
                }
            }
        } while ($Retry)
    
        $version = [uint32]$gpContainer.versionNumber
        $inifilepath = "\\$DomainController\SYSVOL\$DomainFQDN\Policies\{$id}\GPT.INI"
        $version=$version+$IncrementBy
    
        # first update ad
        try {
            Set-ADObject -identity $gpContainer.distinguishedname -Replace @{versionNumber=$version} -ErrorAction Stop -Server $DomainController
        } catch {}
    
        # Then update filesystem (SYSVOL)
        try {
            $count = 0
            Do {
                Start-Sleep -seconds 1
                $count++
                if ($count -gt 60) {
                    throw "Timeout waiting for the creation of '$inifilepath'"
                }
                
            } while (!(Test-Path -path $inifilepath))
            
            $inifile = $Utils.GetIniFile($inifilepath)
            $inifile.General.Version = $version
            $Utils.WriteIniFile($inifilepath, $inifile, 'UTF8NoBOM', $false, $true) # FilePath, InputObject, Encoding, Append, Force
        } catch {
            throw $_
        }
    }

    #[void] hidden AddLink() { }
    #[void] hidden ApplyWMIFilter() { }
    #[void] hidden ApplyGPPermissions() { }

    [void]GetPolicyFromJson(
        [string]$FileName,
        [hashtable]$Replacements,
        [bool]$IsComparing
    ) {
        $AllowedValueTypes = @('REG_MULTI_SZ', 'REG_SZ', 'REG_DWORD', 'REG_QWORD', 'REG_NONE')
        #$AllowedTrusteeTypes = @('User', 'Group', 'Computer')
        $Utils = [Utils]::new()
        $defaultNamingContext = (Get-ADRootDSE).defaultNamingContext
        
        # Read file. If $Replacements, replace
        if ($Replacements -and ($Replacements.count -gt 0)) {
            $jsonRaw = Get-Content -Path $FileName -Encoding Default -Raw -ErrorAction Stop
            foreach ($key in $Replacements.Keys) {
                # $jsonRaw = $jsonRaw.Replace($key,$Replacements["$Key"]) # <-- case sensitive, so ##domainNB## won't match ##DomainNB##
                $jsonRaw = $jsonRaw -replace $key,$Replacements["$Key"]  # <-- case in-sensitive by default, so ##domainNB## will match ##DomainNB##

            }
            $jsonImport = $jsonRaw | ConvertFrom-Json -ErrorAction Stop
        } else {
            $jsonImport = Get-Content -Path $FileName -Encoding Default -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        }

        $this.Name = $jsonImport.Name
        $this.ComputerSettingsEnabled = $jsonImport.ComputerSettingsEnabled
        $this.UserSettingsEnabled = $jsonImport.UserSettingsEnabled
        $this.gPCMachineExtensionNames = $jsonImport.gPCMachineExtensionNames
        $this.gPCUserExtensionNames = $jsonImport.gPCUserExtensionNames
        
        ## GPO Comments
        if ($jsonImport.PolicySettings.GPOComments) {
            foreach ($item in $jsonImport.PolicySettings.GPOComments) {
                #$itemreplaced = $Utils.ReplaceNames($item,$false)
                $this.PolicySettings.GPOComments += $item #$itemreplaced
            }
        }

        ## AdmTemplates Machine Settings Comments
        if ($jsonImport.PolicySettings.MachineComments) {
            foreach ($item in $jsonImport.PolicySettings.MachineComments) {
                $this.PolicySettings.MachineComments += $item 
            }
        }

        ## AdmTemplates User Settings Comments
        if ($jsonImport.PolicySettings.UserComments) {
            foreach ($item in $jsonImport.PolicySettings.UserComments) {
                $this.PolicySettings.UserComments += $item 
            }
        }

        ## Registry settings ##
        if ($jsonImport.PolicySettings.RegistrySettings) {
            foreach ($RegistrySetting in $jsonImport.PolicySettings.RegistrySettings) {
                # Validate input. Make sure correct value types are used. Allowed values are REG_SZ, REG_DWORD, REG_QWORD
                if ($AllowedValueTypes -icontains $RegistrySetting.ValueType) {
                    # Replace any Replace####[names]
                    #$RegistrySetting.ValueName = $Utils.ReplaceNames($RegistrySetting.ValueName,$false)
                    try {
                        $this.PolicySettings.RegistrySettings += [RegistrySetting]$RegistrySetting
                    } catch {
                        throw "Verify registry setting properties. Must include the following properties: Target, KeyName, ValueType, ValueName and ValueData"
                    }
                } else {
                    Write-Warning "Validation error: Registry valuetype = $($RegistrySetting.ValueType) is not allowed. Use one of the following types: $AllowedValueTypes"
                }
            }
        }

        ## Audit settings ##
        if ($jsonImport.PolicySettings.AuditSettings) {
            foreach ($AuditSetting in $jsonImport.PolicySettings.AuditSettings) {
                try {
                    $this.PolicySettings.AuditSettings += [AuditSetting]$AuditSetting
                } catch {
                    throw "Failed to import audit settings from json - $($_.Exception.Message)"
                }
            }
        }

        ## Security template ##
        if ($jsonImport.PolicySettings.SecurityTemplate) {
            foreach ($item in $jsonImport.PolicySettings.SecurityTemplate) {
                #$itemreplaced = $Utils.ReplaceNames($item,$false)
                $this.PolicySettings.SecurityTemplate += $item #$itemreplaced
            }
        }

        ## Folder Redirection version Zero (user only-setting) - this file seems to always be there - but always only 3 blank lines ##
        if ($jsonImport.PolicySettings.FolderRedirection) {
            foreach ($item in $jsonImport.PolicySettings.FolderRedirection) {
                # $itemreplaced = $Utils.ReplaceNames($item,$true)
                # $this.PolicySettings.FolderRedirection += $itemreplaced
                $this.PolicySettings.FolderRedirection += $item
            }
        }
        
        ## Folder Redirection version One (user only-setting) ##
        if ($jsonImport.PolicySettings.FolderRedirection1) {
            foreach ($item1 in $jsonImport.PolicySettings.FolderRedirection1) {
                # $itemreplaced1 = $Utils.ReplaceNames($item1,$true)
                # $this.PolicySettings.FolderRedirection1 += $itemreplaced1
                $this.PolicySettings.FolderRedirection1 += $item1
            }
        }

        ## Scripts ##
        if ($jsonImport.PolicySettings.Scripts) {
            foreach ($Script in $jsonImport.PolicySettings.Scripts) {
                $this.PolicySettings.Scripts = [Script[]]$jsonImport.PolicySettings.Scripts
            }
        }

        ## GPP ##
        if ($jsonImport.PolicySettings.GroupPolicyPreferences) {
            foreach ($GPP in $jsonImport.PolicySettings.GroupPolicyPreferences) {
                $XmlContent = [xml]$GPP.XmlContent
                $XmlContent = $Utils.ReplaceXmlValues($XmlContent, $IsComparing)
                $XmlContent = $Utils.FormatXml($XmlContent, 4)
                #foreach ($line in $XmlContent) {
                # $line = $Utils.ReplaceNames($line,$false)
                #}
                $GPP.XmlContent = $XmlContent
                $this.PolicySettings.GroupPolicyPreferences += [GroupPolicyPreference]$GPP
            }
        }

        # Permissions
        #$this.Permissions = $Utils.ReplaceNames($jsonImport.Permissions,$false)
        $this.Permissions = $jsonImport.Permissions

        # Links
        foreach ($LinkTarget in $jsonImport.LinkTargets) {
            $Target = $LinkTarget.Target -ireplace '####defaultNamingContext####', $defaultNamingContext
            $this.LinkTargets += [LinkTarget]::new($Target, $LinkTarget.LinkOrder, $LinkTarget.LinkPolicy, $LinkTarget.LinkEnabled, $LinkTarget.Enforced) # Target, LinkOrder, LinkPolicy, LinkEnabled, Enforced
        }

        # WMI filter
        if ($jsonImport.WMIFilter) {
            $this.WMIFilter = [WMIFilter]::new($jsonImport.WMIFilter.Name, $jsonImport.WMIFilter.Query, $jsonImport.WMIFilter.Description)
        }
    }

    [void]GetPolicyFromAD(
        [string]$Name,
        [bool]$GetLinks
    ) {
        $Utils = [Utils]::new()
        $this.Name = $Name
        
        Import-Module -Name GPRegistryPolicyParser -ErrorAction Stop -WarningAction Continue
        
        $GPO = Get-GPO -Name $Name
        $PolicyGuid = "{$($GPO.Id.Guid)}"

        $this.ComputerSettingsEnabled = $GPO.Computer.Enabled
        $this.UserSettingsEnabled = $GPO.User.Enabled

        $Domain = [Domain]::GetComputerDomain().Name
        $defaultNamingContext = (Get-ADRootDSE).defaultNamingContext
        $PolicyServerId = (Get-CertificateEnrollmentPolicyServer -Scope All -Context Machine).Id

        ## GPO Comments
        $GPOCommentsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\GPO.cmt"
        if ((Test-Path -Path $GPOCommentsPath)) {
            try {
                $GPOCommentContent = Get-Content -Path $GPOCommentsPath -Encoding unicode -ErrorAction Stop
            } catch {
                throw "Failed to read GPO.cmt - $($_.Exception.Message)"
            }
            $arrGPOCommentContent = [string[]]$GPOCommentContent
            $this.PolicySettings.GPOComments = $arrGPOCommentContent
        }

        ## AdmTemplates Machine Settings Comments
        $MachineCommentsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\comment.cmtx"
        if ((Test-Path -Path $MachineCommentsPath)) {
            try {
                $MachineCommentsContent = Get-Content -Path $MachineCommentsPath -Encoding UTF8 -ErrorAction Stop
            } catch {
                throw "Failed to read Machine\comment.cmtx - $($_.Exception.Message)"
            }
            $arrMachineCommentsContent = [string[]]$MachineCommentsContent
            $this.PolicySettings.MachineComments = $arrMachineCommentsContent
        }

        ## AdmTemplates User Settings Comments
        $UserCommentsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\comment.cmtx"
        if ((Test-Path -Path $UserCommentsPath)) {
            try {
                $UserCommentsContent = Get-Content -Path $UserCommentsPath -Encoding UTF8 -ErrorAction Stop
            } catch {
                throw "Failed to read User\comment.cmtx - $($_.Exception.Message)"
            }
            $arrUserCommentsContent = [string[]]$UserCommentsContent
            $this.PolicySettings.UserComments = $arrUserCommentsContent
        }

        
        ## Registry settings - Machine ##
        $PolFilePath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\Registry.pol"
        if ((Test-Path -Path $PolFilePath)) {
            # Machine policy settings found. Try to read all settings.
            try {
                #$this.PolicySettings = [PolicySettings]::new()
                $ParseResult = Parse-PolFile -Path $PolFilePath
                foreach ($item in $ParseResult) {
                    $item.ValueData = $Utils.ReplaceSIDs($item.ValueData)
                    $item.ValueData = $item.ValueData -ireplace $PolicyServerId, '####PolicyServerId####'
                    $RegSetting = [RegistrySetting]::new('Machine', $item.KeyName, $item.ValueType, $item.ValueName, $item.ValueData)
                    $this.PolicySettings.Add($RegSetting)
                }
            } catch {
                throw "Failed to parse machine registry settings - $($_.Exception.Message)"
            }

        }

        ## Registry settings - User ##
        $PolFilePath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\Registry.pol"
        if ((Test-Path -Path $PolFilePath)) {
            # Machine policy settings found. Try to read all settings.
            try {
                $ParseResult = Parse-PolFile -Path $PolFilePath
                foreach ($item in $ParseResult) {
                    $item.ValueData = $Utils.ReplaceSIDs($item.ValueData)
                    $RegSetting = [RegistrySetting]::new('User', $item.KeyName, $item.ValueType, $item.ValueName, $item.ValueData)
                    $this.PolicySettings.Add($RegSetting)
                }
            } catch {
                throw "Failed to parse user registry settings - $($_.Exception.Message)"
            }
        }


        ## Audit settings ##
        $AuditPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\Microsoft\Windows NT\Audit\audit.csv"
        if ((Test-Path -Path $AuditPath)) {
            try {
                $AuditCSV = Import-Csv -Path $AuditPath -Delimiter "," -Encoding Default
            } catch {
                throw "Failed to read audit.csv - $($_.Exception.Message)"
            }

            foreach ($item in $AuditCSV) {
                try {
                    # Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value
                    $AuditSetting = [AuditSetting]::new($item.{Machine Name}, $item.{Policy Target}, $item.Subcategory, $item.{SubCategory GUID}, $item.{Inclusion Setting}, $item.{Exclusion Setting}, $item.{Setting Value}) 
                    $this.PolicySettings.Add($AuditSetting)
                } catch {
                    throw "Failed to add audit settings - $($_.Exception.Message)"
                }
            }
        }

        ## Security template ##
        $SecurityTemplatePath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\Microsoft\Windows NT\SecEdit\GptTmpl.inf"
        if ((Test-Path -Path $SecurityTemplatePath)) {
            try {
                $FileContent = Get-Content -Path $SecurityTemplatePath -ErrorAction Stop
            } catch {
                throw "Failed to read GptTeml.inf - $($_.Exception.Message)"
            }
            $arrFileContent = [string[]]$FileContent
            $Utils = [Utils]::new()
            for ($i = 0; $i -lt $arrFileContent.Count; $i++) {
                $arrFileContent[$i] = $Utils.ReplaceSIDs($arrFileContent[$i])
            }
            $this.PolicySettings.SecurityTemplate = $arrFileContent
        }

        ## Folder Redirection version Zero (user only-setting) - this file seems to always be there - but always empty ##
        $FolderRedirectionPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\Documents & Settings\fdeploy.ini"
        if ((Test-Path -Path $FolderRedirectionPath)) {
            try {
                $FRContent = Get-Content -Path $FolderRedirectionPath -ErrorAction Stop
            } catch {
                throw "Failed to read fdeploy.ini and/or fdeploy.ini - $($_.Exception.Message)"
            }

            $arrFRContent = [string[]]$FRContent
            $Utils = [Utils]::new()
            for ($i = 0; $i -lt $arrFRContent.Count; $i++) {
                $arrFRContent[$i] = $Utils.ReplaceSIDs($arrFRContent[$i])
            }
            $this.PolicySettings.FolderRedirection = $arrFRContent
        }

        ## Folder Redirection version One (user only-setting) ##
        $FolderRedirectionPath1 = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\Documents & Settings\fdeploy1.ini"
        if ((Test-Path -Path $FolderRedirectionPath1)) {
            try {
                $FRContent1 = Get-Content -Path $FolderRedirectionPath1 -ErrorAction Stop
            } catch {
                throw "Failed to read fdeploy.ini and/or fdeploy1.ini - $($_.Exception.Message)"
            }

            $arrFRContent1 = [string[]]$FRContent1
            $Utils = [Utils]::new()
            for ($i = 0; $i -lt $arrFRContent1.Count; $i++) {
                $arrFRContent1[$i] = $Utils.ReplaceSIDs($arrFRContent1[$i])
            }
            $this.PolicySettings.FolderRedirection1 = $arrFRContent1
        }

        ## Scripts ##
        $GPSource = @('Machine', 'User')
        foreach ($source in $GPSource) {
            $ScriptsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\$source\Scripts"
            if ((Test-Path -Path $ScriptsPath)) {
                $NewScript = [Script]::new()
                $NewScript.Target = $source
                
                # Read psscripts.ini
                if ((Test-Path -Path $ScriptsPath\psscripts.ini)) {
                    $NewScript.PSScriptsIni = $Utils.GetIniFile("$ScriptsPath\psscripts.ini")
                }

                # Read scripts.ini
                if ((Test-Path -Path $ScriptsPath\scripts.ini)) {
                    $NewScript.ScriptsIni = $Utils.GetIniFile("$ScriptsPath\scripts.ini")
                }

                # Read scripts from subfolders. Possible subfolders are Shutdown, Startup, Logon and Logoff
                $ScriptFolders = Get-ChildItem -Path $ScriptsPath -Directory -Force
                foreach ($folder in $ScriptFolders) {
                    $FoundScripts = Get-ChildItem -Path $folder.FullName -File -Force
                    foreach ($FoundScript in $FoundScripts) {
                        $NewScriptFile = [ScriptFile]::new()
                        $NewScriptFile.Type = $folder.BaseName
                        $NewScriptFile.Name = $FoundScript.Name
                        $NewScriptFile.Content = Get-Content -Path $FoundScript.FullName -Encoding Default -ErrorAction Stop
                        $NewScript.ScriptFiles += $NewScriptFile
                    }
                }

                $this.PolicySettings.Scripts += $NewScript
            }
        }

        ## GPP ##
        $GPSource = @('Machine', 'User')
        foreach ($source in $GPSource) {
            $GPPPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\$source\Preferences"
            if ((Test-Path -Path $GPPPath)) {
                $Folders = Get-ChildItem -Path $GPPPath -Directory -Force
                foreach ($Folder in $Folders) {
                    $GPP = [GroupPolicyPreference]::new()
                    $GPP.Type = $Folder.Name
                    $GPP.Target = $source
                    
                    $Files = Get-ChildItem -Path $Folder.FullName -File -Force
                    foreach ($File in $Files) {
                        $GPP.XmlFileName = $File.Name
                        try {
                            $Xml = [xml](Get-Content -Path ($File.FullName) -Encoding Default -ErrorAction Stop)
                            $XmlContent = $Utils.FormatXml($Xml, 4)

                            # Replace any SIDs in xml
                            for ($XmlCi = 0; $XmlCi -lt $XmlContent.count; $XmlCi++) {
                                $XmlContent[$XmlCi] = $Utils.ReplaceSIDs("$($XmlContent[$XmlCi])")
                            }

                            $GPP.XmlContent = $XmlContent
                        } catch {
                            throw "Failed to get GGP file $($Folder.Name).xml - $($_.Exception.Message)"
                        }
                        
                        $this.PolicySettings.GroupPolicyPreferences += $GPP
                    }
                }
            }
        }


        # Get Client side extensions
        $gpcObject = Get-ADObject -Filter "ObjectClass -eq 'groupPolicyContainer' -and Name -eq '$PolicyGuid'" -Properties gPCMachineExtensionNames,gPCUserExtensionNames
        if ($gpcObject) {
            # Get Client Side Extensions
            if ($gpcObject.gPCMachineExtensionNames) {
                $this.gPCMachineExtensionNames = $gpcObject.gPCMachineExtensionNames
            }

            if ($gpcObject.gPCUserExtensionNames) {
                $this.gPCUserExtensionNames = $gpcObject.gPCUserExtensionNames
            }

            # Get permissions. This includes security filtering AND delegation
            $acl = Get-Acl "AD:$($gpcObject.DistinguishedName)"
            $sddl = $acl.sddl
            $sddl = $utils.ReplaceSIDs($sddl)
            $this.Permissions = $sddl
        }

        # Get all links with link order
        if ($GetLinks) {
            $dn = $gpcObject.distinguishedName
            $AllLinkLocations = @(Get-ADObject -Filter "(objectClass -eq 'organizationalUnit' -or objectClass -eq 'domain' -or objectClass -eq 'site') -and gpLink -like '*$dn*'" -Properties gPLink)
            $AllLinkLocations += Get-ADObject -Filter "objectClass -eq 'site' -and gpLink -like '*$dn*'" -SearchBase "CN=Sites,$((Get-ADRootDSE).configurationNamingContext)" -Properties gPLink

            foreach ($item in $AllLinkLocations) { 
                $dn = $item.distinguishedName -ireplace $defaultNamingContext,'####defaultNamingContext####'
                $arrPolicies = $item.gPLink.Split('][', [System.StringSplitOptions]::RemoveEmptyEntries)

                $LinkOrder = 0
                $LinkPolicy = $false
                $LinkEnabled = $false
                $Enforced = $false
                for ($i = 0; $i -lt $arrPolicies.Count; $i++) {
                    if ($arrPolicies[$i] -match $PolicyGUID) {
                        $LinkOrder = $i + 1
                        
                        # 0 = Enabled
                        # 1 = Disabled
                        # 2 = Enabled + Enforced
                        # 3 = Disabled + Enforced
                        $Status = $arrPolicies[$i].Split(';')[1]
                        switch ($Status) {
                            0 {
                                $LinkPolicy = $true
                                $LinkEnabled = $true
                                $Enforced = $false
                            }
                            1 {
                                $LinkPolicy = $true
                                $LinkEnabled = $false
                                $Enforced = $false
                            }
                            2 {
                                $LinkPolicy = $true
                                $LinkEnabled = $true
                                $Enforced = $true
                            }
                            3 {
                                $LinkPolicy = $true
                                $LinkEnabled = $false
                                $Enforced = $true
                            }
                        }
                    }
                }
                $this.LinkTargets += [LinkTarget]::new($dn, $LinkOrder, $LinkPolicy, $LinkEnabled, $Enforced) # Target, LinkOrder, LinkPolicy, LinkEnabled, Enforced
            }
        }

        # Get WMI filter
        if ($GPO.WMIFilter) {
            $WMIFilterObject = Get-ADObject -Filter "objectClass -eq 'msWMI-Som' -and msWMI-Name -eq '$($GPO.WMIFilter.Name)'" -Properties msWMI-Parm1,msWMI-Parm2 -ErrorAction SilentlyContinue
            $this.WMIFilter = [WMIFilter]::new($GPO.WMIFilter.Name, $WMIFilterObject.'msWMI-Parm2', $WMIFilterObject.'msWMI-Parm1') # Name, Query, Description
        } else {
            $this.WMIFilter = $null
        }
    }

    [void]WritePolicyToJson(
        $FileName
    ) {
        try {
            $this | ConvertTo-Json -Depth 10 | Out-File -FilePath $FileName -Encoding default
        } catch {
            throw $_
        }
    }
    
    [void]WritePolicyToAD(
        [bool]$Overwrite,
        [bool]$Backup,
        [bool]$RemoveLink,
        [bool]$DoNotLinkGPO,
        [string]$DomainController
    ) {
        $Utils = [Utils]::new()
        $Domain = [Domain]::GetComputerDomain().Name
        $defaultNamingContext = (Get-ADRootDSE).defaultNamingContext
        $PolicyServerId = (Get-CertificateEnrollmentPolicyServer -Scope All -Context Machine).Id
        
        # Does it exist a GPO with the same name?
        $GPO = Get-GPO -Name $this.Name -ErrorAction SilentlyContinue -Server $DomainController
        if ($GPO) {
            # Yes, it exists
            # What to do? Quit, delete or rename and disable link, then create a new GPO.

            if (!$Overwrite) { 
                return 
            }

            if ($Backup) {
                # Disable link
                # First, find all OUs where the GPO is linked.
                $LinkedOUs = @()
                $AllOUs = Get-ADObject -Filter "objectClass -eq 'organizationalUnit' -or objectClass -eq 'domain' -or objectClass -eq 'site'" -Properties gPLink -Server $DomainController
                $AllOUs += Get-ADObject -Filter "objectClass -eq 'site'" -SearchBase "CN=Sites,$((Get-ADRootDSE).configurationNamingContext)" -Properties gPLink -Server $DomainController

                for ($i = 0; $i -lt $AllOUs.Count; $i++) {
                    $gPLink = $AllOUs[$i].gPLink
                    
                    if ($gPLink) {
                        $arrGPLink = @($gPLink.Split('][',[System.StringSplitOptions]::RemoveEmptyEntries))

                        foreach ($item in $arrGPLink) {
                            if ($item -imatch $GPO.Path) { 
                                $LinkedOUs += $AllOUs[$i].distinguishedName
                            }
                        }
                    }
                }

                foreach ($item in $LinkedOUs) {
                    try {
                        if ($RemoveLink) {
                            Remove-GPLink -Name $this.Name -Target $item -Confirm:$false -Server $DomainController -ErrorAction Stop
                        } else {
                            Set-GPLink -Name $this.Name -Target $item -LinkEnabled No -Server $DomainController -ErrorAction Stop
                        }
                    } catch {
                        throw $_
                    }
                }

                # Rename GPO
                try {
                    Rename-GPO -Name $this.Name -TargetName "$($this.Name) - Backup $(Get-Date -Format 'yyyy-MM-dd hh:mm')" -Server $DomainController -ErrorAction Stop
                } catch {
                    throw $_
                }
            } else {
                # No backup specified. Delete the old GPO.
                try {
                    Remove-GPO -Name $this.Name -Server $DomainController -Confirm:$false -ErrorAction Stop
                } catch {
                    throw "Failed to delete GPO $($this.Name) - $($_.Exception.Message)"
                }
            }
        } else {
            # No, it does NOT exist. Safe to continue.
        }

        try {
            $GPO = New-GPO -Name $this.Name -Server $DomainController -ErrorAction Stop
            $PolicyGuid = "{$($GPO.Id.Guid)}"
        } catch {
            # Something failed
            throw $_
        }

        # Apply GPO Comments
        if ($this.PolicySettings.GPOComments) {
             # The file can contain no more than 2047 carachters, where each line break counts as 2 (\r\n)
            if ($Utils.CommentInSpec($this.PolicySettings.GPOComments)) {
                $GPOCommentsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\GPO.cmt"
                
                try {
                    $this.PolicySettings.GPOComments | Out-File -FilePath $GPOCommentsPath -Encoding unicode
                } catch {
                    throw "Failed to save GPO.cmt - $($_.Exception.Message)"
                }
            }
            else {
                Write-Warning "The GPO Comment section may only contain 2047 carachters (new line counts 2)"
                throw "The GPO Comment section may only contain 2047 carachters (new line counts 2)"
            } 
        }

        # Apply Administrative Templates Machine Settings Comments
        if ($this.PolicySettings.MachineComments) {
            $MachineCommentsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\comment.cmtx"
            try {
                $this.PolicySettings.MachineComments | Out-File -FilePath $MachineCommentsPath -Encoding UTF8
            } catch {
                throw "Failed to save Machine\comment.cmtx - $($_.Exception.Message)"
            } 
        }

       # Apply Administrative Templates User Settings Comments
       if ($this.PolicySettings.UserComments) {
            $UserCommentsPath = "\\$($Domain)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\comment.cmtx"
            try {
                $this.PolicySettings.UserComments | Out-File -FilePath $UserCommentsPath -Encoding UTF8
            } catch {
                throw "Failed to save User\comment.cmtx - $($_.Exception.Message)"
            } 
        }
        
        # Apply registry settings if specified
        if ($this.PolicySettings.RegistrySettings) {
            # Registry settings found in policy. Starting to apply them.

            Import-Module -Name GPRegistryPolicyParser -ErrorAction Stop -WarningAction Continue
        
            $MachinePolFileExists = $false
            $UserPolFileExists = $false
            $arrMachinePolicies = @()
            $arrUserPolicies = @()
            $polMachinePath = $null
            $polUserPath = $null

            foreach ($setting in $this.PolicySettings.RegistrySettings) {
                switch ($Setting.Target.ToLower()) {
                    'machine' {
                        $polMachinePath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\Registry.pol"

                        if ((Test-Path -Path $polMachinePath) -eq $false -and $MachinePolFileExists -eq $false) {
                            Create-GPRegistryPolicyFile -Path $polMachinePath
                            $MachinePolFileExists = $true
                        }
                    }
                    'user' {
                        $polUserPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\Registry.pol"

                        if ((Test-Path -Path $polUserPath) -eq $false -and $UserPolFileExists -eq $false) {
                            Create-GPRegistryPolicyFile -Path $polUserPath
                            $UserPolFileExists = $true
                        }
                    }
                    default { throw 'Wrong Target type. Must be Machine or User.' }
                }

                $RegSettings = @{}
                $RegSettings.Add("keyName",$setting.KeyName)
                $RegSettings.Add("valueType",$setting.ValueType)
                if ($null -ne $setting.ValueName) 
                { 
                    if ($Setting.ValueName.Length -eq 1) {
                        if (([byte][char]$setting.ValueName) -ne 0) {
                            $RegSettings.Add("valueName",$setting.ValueName)     
                        } else {
                            $RegSettings.Add("valueName","")
                        }
                    } else {
                        $RegSettings.Add("valueName",$setting.ValueName) 
                    }
                }

                if ($null -ne $setting.ValueData) {
                    $ValueData = $null
                    switch ($setting.ValueType.ToLower()) {
                        "reg_dword" { $ValueData = [uint32]$Setting.ValueData }
                        "reg_qword" { $ValueData = [uint64]$Setting.ValueData }
                        default { 
                            $ValueData = $Utils.ReplaceNames($Setting.ValueData,$false)
                            $ValueData = $ValueData -ireplace '####PolicyServerId####', $PolicyServerId
                        }
                        
                    }
                    
                    $Regsettings.Add("valueData", $ValueData)
                }
                    
                if ($setting.Target -imatch 'machine') { $arrMachinePolicies += New-GPRegistryPolicy @RegSettings }
                if ($setting.Target -imatch 'user') { $arrUserPolicies += New-GPRegistryPolicy @RegSettings }
            }

            # Apply machine policy
            if ($polMachinePath) {
                try {
                    Append-RegistryPolicies -Path $polMachinePath -RegistryPolicies $arrMachinePolicies
                } catch {
                    Write-Error "Failed to set machine settings - $($_.Exception.Message)"
                }
            }

            # Apply machine policy
            if ($polUserPath) {
                try {
                    Append-RegistryPolicies -Path $polUserPath -RegistryPolicies $arrUserPolicies
                } catch {
                    Write-Error "Failed to set user settings - $($_.Exception.Message)"
                }
            }
        }

        # Audit
        if ($this.PolicySettings.AuditSettings) {
            $AuditPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\Microsoft\Windows NT\Audit"
            if ( (Test-Path -Path $AuditPath) -eq $false ) { New-Item -Path $AuditPath -ItemType Directory  }
            
            # The ObjectType property is meta information, not part of audit.csv
            $AuditFileContent = $this.PolicySettings.AuditSettings | 
            Select-Object -Property * -ExcludeProperty ObjectType | 
            ConvertTo-Csv -NoTypeInformation -Delimiter "," | 
            foreach-Object { $_.Replace('"', '') } 
            
            try {
                $AuditFileContent | Out-File -FilePath $AuditPath\audit.csv -Encoding utf8 -ErrorAction Stop
            } catch {
                throw "Failed to save audit.csv - $($_.Exception.Message)"
            }
        }

        # Security Template
        if ($this.PolicySettings.SecurityTemplate) {
            $SecPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\Machine\Microsoft\Windows NT\SecEdit"
            if ( (Test-Path -Path $SecPath) -eq $false ) { New-Item -Path $SecPath -ItemType Directory  }

            # Replace names with SIDs
            for ($i = 0; $i -lt $this.PolicySettings.SecurityTemplate.Count; $i++) {
                $this.PolicySettings.SecurityTemplate[$i] = $Utils.ReplaceNames($this.PolicySettings.SecurityTemplate[$i],$false)
            }
            #foreach ($line in $this.PolicySettings.SecurityTemplate) {
            # $line = $Utils.ReplaceNames($line,$false)
            #}

            try {
                $this.PolicySettings.SecurityTemplate | Out-File -FilePath $SecPath\GptTmpl.inf -Encoding default
            } catch {
                throw "Failed to save GptTempl.inf - $($_.Exception.Message)"
            }
        }

        # Folder Redirection v Zero
        if ($this.PolicySettings.FolderRedirection) {
            $FRPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\Documents & Settings"
            if ( (Test-Path -Path $FRPath) -eq $false ) { New-Item -Path $FRPath -ItemType Directory  }

            # Replace names with lower-cased-SIDs
            for ($i = 0; $i -lt $this.PolicySettings.FolderRedirection.Count; $i++) {
                $this.PolicySettings.FolderRedirection[$i] = $Utils.ReplaceNames($this.PolicySettings.FolderRedirection[$i],$true)
            }

            try {
                $this.PolicySettings.FolderRedirection | Out-File -FilePath $FRPath\fdeploy.ini -Encoding unicode
            } catch {
                throw "Failed to save fdeploy.ini - $($_.Exception.Message)"
            }
        }

        # Folder Redirection v One
        if ($this.PolicySettings.FolderRedirection1) {
            $FRPath1 = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\User\Documents & Settings"
            if ( (Test-Path -Path $FRPath1) -eq $false ) { New-Item -Path $FRPath1 -ItemType Directory  }

            # Replace names with lower-cased-SIDs
            for ($i = 0; $i -lt $this.PolicySettings.FolderRedirection1.Count; $i++) {
                $this.PolicySettings.FolderRedirection1[$i] = $Utils.ReplaceNames($this.PolicySettings.FolderRedirection1[$i],$true)
            }

            try {
                $this.PolicySettings.FolderRedirection1 | Out-File -FilePath $FRPath1\fdeploy1.ini -Encoding unicode
            } catch {
                throw "Failed to save fdeploy1.ini - $($_.Exception.Message)"
            }
        }

        # Scripts
        if ($this.PolicySettings.Scripts) {
            foreach ($Script in $this.PolicySettings.Scripts) {
                $source = $Script.Target
                $ScriptsPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\$source\Scripts"

                if ((Test-Path -Path $ScriptsPath) -eq $false) {
                    try {
                        New-Item -Path $ScriptsPath -Type Directory -Force -ErrorAction Stop
                    } catch {
                        throw "Failed to create $source scripts directory - $($_.Exception.Message)"
                    }

                    # Create psscripts.ini
                    $PSScriptsIniHash = $Utils.ConvertPSCustomObjectToHashTable($Script.PSScriptsIni)
                    $Utils.WriteIniFile("$ScriptsPath\psscripts.ini", $PSScriptsIniHash, "UTF8", $false, $true) # FilePath, InputObject, Encoding, Append, Force
            
                    # Create scripts.ini
                    $ScriptsIniHash = $Utils.ConvertPSCustomObjectToHashTable($Script.ScriptsIni)
                    $Utils.WriteIniFile("$ScriptsPath\scripts.ini", $ScriptsIniHash, "UTF8", $false, $true) # FilePath, InputObject, Encoding, Append, Force

                    # Create subfolders and scripts
                    foreach ($ScriptFile in $Script.ScriptFiles) {
                        if ((Test-Path -Path "$ScriptsPath\$($ScriptFile.Type)") -eq $false) {
                            try {
                                New-Item -Path "$ScriptsPath\$($ScriptFile.Type)" -Type Directory -Force -ErrorAction Stop
                            } catch {
                                throw "Failed to create $($ScriptFile.Type) script directory - $($_.Exception.Message)"
                            }

                            try {
                                $ScriptFile.Content | Out-File -FilePath "$ScriptsPath\$($ScriptFile.Type)\$($ScriptFile.Name)" -Encoding default -ErrorAction Stop
                            } catch {
                                throw "Failed to save script $($ScriptFile.FileName) - $($_.Exception.Message)"
                            }
                        }
                    }
                }
            }
        }

        # GPP
        if ($this.PolicySettings.GroupPolicyPreferences) {
            foreach ($GPP in $this.PolicySettings.GroupPolicyPreferences) {
                $GPPRootPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\$($GPP.Target)\Preferences"
                if ( (Test-Path -Path $GPPRootPath) -eq $false ) { New-Item -Path $GPPRootPath -ItemType Directory }

                $GPPPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\$($GPP.Target)\Preferences\$($GPP.Type)"
                if ( (Test-Path -Path $GPPPath) -eq $false ) { New-Item -Path $GPPPath -ItemType Directory }

                $XmlPath = "\\$($DomainController)\SYSVOL\$($Domain)\Policies\$($PolicyGuid)\$($GPP.Target)\Preferences\$($GPP.Type)\$($GPP.XmlFileName)"
                try {
                    for ($i = 0; $i -lt $GPP.XmlContent.Count; $i++) {
                        $GPP.XmlContent[$i] = $Utils.ReplaceNames($GPP.XmlContent[$i],$false)
                    }
                    #foreach ($line in $GPP.XmlContent) {
                    # $line = $Utils.ReplaceNames($line,$false)
                    #}

                    $GPP.XmlContent | Out-File -FilePath $XmlPath -Encoding utf8 -ErrorAction Stop
                } catch {
                    throw "Failed to write Xml file $($GPP.FileName) - $($_.Exception.Message)"
                }
            }
        }

        # Client Side Extensions
        $gpcObject = $null
        $gpcObject = Get-ADObject -Filter "ObjectClass -eq 'groupPolicyContainer' -and Name -eq '$PolicyGuid'" -Server $DomainController -ErrorAction SilentlyContinue
        # $IncrementBy specifies how much to increment the gpcontainer property 'versionNumber' with
        [uint32]$IncrementBy = 0

        if ($gpcObject) {
            if ($this.gPCMachineExtensionNames) {
                try {
                    #Set-ADObject -Identity $gpcObject.distinguishedName -gPCMachineExtensionNames $this.gPCMachineExtensionNames -Server $DomainController -ErrorAction Stop
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{gPCMachineExtensionNames=$this.gPCMachineExtensionNames} -ErrorAction 'Stop' -Server $DomainController
                    # The GPO contains machine settings - add 1 to $IncrementBy (1 is one machine version up)
                    $IncrementBy++
                } catch {
                    throw "Failed to save client side extension for machine - $($_.Exception.Message)"
                }
            }

            if ($this.gPCUserExtensionNames) {
                try {
                    #Set-ADObject -Identity $gpcObject.distinguishedName -gPCUserExtensionNames $this.gPCUserExtensionNames -Server $DomainController -ErrorAction Stop
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{gPCUserExtensionNames=$this.gPCUserExtensionNames} -ErrorAction 'Stop' -Server $DomainController
                    # The GPO contains user settings - add 65536 to $IncrementBy (65536 is one user version up)
                    $IncrementBy = $IncrementBy + 65536
                } catch {
                    throw "Failed to save client side extension for user - $($_.Exception.Message)"
                }
            }
            
            # Enable / disable user and computersettings as needed
            # flags:
            # 0 - Enabled
            # 1 - User settings disabled
            # 2 - Computer settings disabled
            # 3 - All settings disabled

            if ($this.ComputerSettingsEnabled -and $this.UserSettingsEnabled) {
                try {
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{flags=0} -ErrorAction 'Stop' -Server $DomainController
                } catch {
                    throw "Failed to save flags - $($_.Exception.Message)"
                }
            }
            
            if ($this.ComputerSettingsEnabled -and $this.UserSettingsEnabled -eq $false) {
                try {
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{flags=1} -ErrorAction 'Stop' -Server $DomainController
                } catch {
                    throw "Failed to save flags - $($_.Exception.Message)"
                }
            }

            if ($this.ComputerSettingsEnabled -eq $false -and $this.UserSettingsEnabled) {
                try {
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{flags=2} -ErrorAction 'Stop' -Server $DomainController
                } catch {
                    throw "Failed to save flags - $($_.Exception.Message)"
                }
            }

            if ($this.ComputerSettingsEnabled -eq $false -and $this.UserSettingsEnabled -eq $false) {
                try {
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{flags=3} -ErrorAction 'Stop' -Server $DomainController
                } catch {
                    throw "Failed to save flags - $($_.Exception.Message)"
                }
            }

            # Apply Permissions
            if ($this.Permissions) {
                $acl = Get-Acl "AD:$($gpcObject.distinguishedname)"
                try {
                    $sddl = $Utils.ReplaceNames($this.Permissions,$false)
                    $acl.SetSecurityDescriptorSddlForm($sddl)
                    Set-Acl -Path "AD:$($gpcObject.distinguishedname)" -AclObject $acl
                } catch {
                    throw "Failed to set security descriptor - $($_.Exception.Message)"
                }
            }
        }
        
        # Link the policy to the listed OUs
        if ($DoNotLinkGPO -eq $false) {
            foreach ($LinkTarget in $this.LinkTargets) {
                if ($LinkTarget.LinkPolicy) {
                    try {
                        if ($LinkTarget.LinkEnabled) { $bEnabled = 'Yes' } else { $bEnabled = 'No' }
                        if ($LinkTarget.Enforced) { $bEnforced = 'Yes' } else { $bEnforced = 'No' }
                        $Target = $LinkTarget.Target -ireplace '####defaultNamingContext####', $defaultNamingContext
                        New-GPLink -Name $this.Name -Target $Target -Order $LinkTarget.LinkOrder -LinkEnabled $bEnabled -Enforced $bEnforced -Server $DomainController
                    } catch {
                        throw "Failed to link GPO to $($LinkTarget.Target) - $($_.Exception.Message)"
                    }
                }
            }
        }

        # Apply WMI filter
        if ($this.WMIFilter) {
            $WMIFilterObject = Get-ADObject -Filter "objectClass -eq 'msWMI-Som' -and msWMI-Name -eq '$($this.WMIFilter.Name)'" -ErrorAction SilentlyContinue -Server $DomainController
            if (!$WMIFilterObject) {
                # WMI Filter does not exist. Create it.
                try {
                    $WMIFilterObject = $this.CreateWMIFilter($this.WMIFilter, $DomainController)
                } catch {
                    throw "Failed to create WMI filter - $($_.Exception.Message)"
                }
            }

            if ($WMIFilterObject) {
                $gPCWQLFilter = "[$Domain;$($WMIFilterObject.name);0]" # gPCWQLFilter = [test.local;{9A1CCFB1-235D-4CF5-B349-D9520D38DBF3};0]
                try {
                    Set-ADObject -identity $gpcObject.distinguishedname -Replace @{gPCWQLFilter=$gPCWQLFilter} -ErrorAction 'Stop' -Server $DomainController
                } catch {
                    throw "Failed to assign WMI filter - $($_.Exception.Message)"
                }
            } else {
                Write-Warning "WMI filter '$this.WMIFilter' was not found"
            }
        }

        # Increment policy version number
        try {
            $this.IncrementVersion($GPO.Id, $DomainController, $Domain, $IncrementBy)
        } catch {
            throw "Failed to increment policy version - $($_.Exception.Message)"
        }
    }

    [object] hidden CreateWMIFilter (
        [WMIFilter]$Filter,
        [string]$DomainController
    ) {
        $Domain = [Domain]::GetCurrentDomain()
        $DomainDN = $Domain.GetDirectoryEntry() | Select-Object -ExpandProperty DistinguishedName
        $guid = ("{" + (New-Guid).Guid + "}").ToUpper()
        $Path = "CN=SOM,CN=WMIPolicy,CN=System,$DomainDN"
        $distinguishedName = "CN=$guid,$Path"
        
        $now = (Get-Date).ToUniversalTime()
        $year = ($now.Year).ToString("0000")
        $month = ($now.month).ToString("00")
        $day = ($now.day).ToString("00")
        $hour = ($now.hour).ToString("00")
        $minute = ($now.minute).ToString("00")
        $second = ($now.second).ToString("00")
        $millisecond = ($now.millisecond * 1000).ToString("000000")
        $CreationDate = "$year$month$day$hour$minute$second.$millisecond-000" #20191023212335.425000-000
        
        $OtherAttribs = @{
            cn = $guid
            distinguishedName = $distinguishedName
            instanceType = 4
            showInAdvancedViewOnly = 'TRUE'
            'msWMI-Name' = $Filter.Name
            'msWMI-Parm2' = $Filter.Query
            'msWMI-Author' = 'robert@test.local'
            'msWMI-ID' = $guid
            'msWMI-ChangeDate' = $CreationDate
            'msWMI-CreationDate' = $CreationDate
        }

        if ($Filter.Description) { $OtherAttribs.Add('msWMI-Parm1', $Filter.Description) }
        
        try {
            $result = New-ADObject -Name $guid -Type 'msWMI-Som' -Path $Path -OtherAttributes $OtherAttribs -PassThru -Server $DomainController
        } catch {
            throw $_
        }
        
        return $result
    }

    [void]RemoveGroupPolicyFromAD(
        [string]$DomainController
    ) {
        try {
            $GPO = Get-GPO -Name $this.Name -Server $DomainController -ErrorAction 'Ignore' 
            if ($GPO) {
                $GPO | Remove-GPO -Server $DomainController -ErrorAction 'Stop'
            }
        } 
        catch {
            throw $_
        }
    }
}

class Utils : BaseSettings {
    Utils () {
        $this.ObjectType = "Utils"
    }

    [PSCustomObject]GetIniFile (
        [string[]]$FilePath
    ) {
        $ini = @{}
        $file = Get-ChildItem -path $FilePath -Force
        if(!(Test-Path $File.FullName)) {
            throw "$($File.FullName) - File not found"
        }
        
        $CommentCount = $null
        $NameValue = $null
        $Section = $null
        switch -regex -file $filepath {
            "^\[(.+)\]" { # section
                $section = $Matches[1]
                $ini[$section] = @{}
                $CommentCount = 0
            }
            
            "^(;.*)$" { # comment
                $Value = $Matches[1]
                $CommentCount = $CommentCount + 1
                $NameValue = "Comment" + $CommentCount
                $Ini[$Section][$NameValue] = $Value
            }

            "(.+?)\s*=(.*)" { # key
                $NameValue,$Value = $Matches[1..2]
                if ($value.Gettype().name -eq "String") {
                    $Value = $value.Trim()
                }
                
                if ($Value -eq "true") {
                    $Value = $true
                } elseif ($Value -eq "false") {
                    $Value = $false
                }
                
                $ini[$Section][$NameValue] = $Value
            }
        }
        
        return $ini
    }

    [PSCustomObject]GetSecurityTemplateContent (
        [string[]]$Content
    ) {
        $ini = @{}

        $CommentCount = $null
        $ValueCount = $null
        $NameValue = $null
        $Section = $null
        switch -regex ($Content) {
            "^\[(.+)\]" { # section
                $section = $Matches[1]
                $ini[$section] = @{}
                $CommentCount = 0
                $ValueCount = 0
            }
            
            "^(;.*)$" { # comment
                $Value = $Matches[1]
                $CommentCount = $CommentCount + 1
                $NameValue = "Comment" + $CommentCount
                $Ini[$Section][$NameValue] = $Value
            }

            #"^((?!\[).)*$" { # key
            "^((?!\[(.+)\]).)*$" { # key
                $Value = $Matches[0]
                $ValueCount++
                $NameValue = "Value" + $ValueCount
                $ini[$Section][$NameValue] = $Value
            }
        }
        
        return $ini
    }

    [void]WriteIniFile (
        [string]$FilePath,
        [Hashtable]$InputObject, 
        [string]$Encoding, 
        [bool]$Append, 
        [bool]$Force 
    ) {
        $FileEncoding = $null
        
        switch ($Encoding) {
            "Unicode" { $FileEncoding = New-Object System.Text.UnicodeEncoding }
            "UTF7" { $FileEncoding = New-Object System.Text.UTF7Encoding }
            "UTF8" { $FileEncoding = New-Object System.Text.UTF8Encoding }
            "UTF8NoBOM" { $FileEncoding = New-Object System.Text.UTF8Encoding($false) }
            "UTF32" { $FileEncoding = New-Object System.Text.UTF32Encoding }
            "ASCII" { $FileEncoding = New-Object System.Text.ASCIIEncoding }
            "BigEndianUnicode" { $FileEncoding = New-Object [System.Text.Encoding]::BigEndianUnicode }
            "Default" { $FileEncoding = New-Object [System.Text.Encoding]::Default  }
            "OEM" { $FileEncoding = New-Object [System.Text.Encoding]::OEM }
        }
        
        if ($append) {
            $OutFile = Get-Item $FilePath
        } else {
            $OutFile = New-Item -ItemType file -Path $Filepath -Force:$Force
        } 
        
        foreach ($i in $InputObject.keys) {
            if (!($($InputObject[$i].GetType().Name) -eq "Hashtable")) {
                #No Sections
                $Errorcount = 0
                do {
                    try {
                        $Lineout = "$i=$($InputObject[$i])" + "`r`n"
                        [System.IO.File]::AppendAllText($OutFile,$LineOut,$FileEncoding)
                        $Errorcount = 100
                    } catch {
                        Start-Sleep -Milliseconds 500
                        $Errorcount++
                    }
                } while ($Errorcount -lt 10)
                
            } else {
                #Sections
                $Errorcount = 0
                do {
                    try {
                        $Lineout = "[$i]" + "`r`n"
                        [System.IO.File]::AppendAllText($OutFile,$LineOut,$FileEncoding)
                        $Errorcount = 100
                    } catch {
                        Start-Sleep -Milliseconds 500
                        $Errorcount++
                    }
                } while ($Errorcount -lt 10)
                
                foreach ($j in $($InputObject[$i].keys | Sort-Object)) {
                    if ($j -match "^Comment[\d]+") {
                        $Errorcount = 0
                        do {
                            try {
                                $Lineout = "$($InputObject[$i][$j])" + "`r`n"
                                [System.IO.File]::AppendAllText($OutFile,$LineOut,$FileEncoding)
                                $Errorcount = 100
                            } catch {
                                Start-Sleep -Milliseconds 500
                                $Errorcount++
                            }
                        } 
                        while ($Errorcount -lt 10)
                    } else {
                        $Errorcount = 0
                        do {
                            try {
                                $Lineout = "$j=$($InputObject[$i][$j])" + "`r`n"
                                [System.IO.File]::AppendAllText($OutFile,$LineOut,$FileEncoding)
                                $Errorcount = 100
                            } catch {
                                Start-Sleep -Milliseconds 500
                                $Errorcount++
                            }
                        } while ($Errorcount -lt 10)
                    }   
                } 
                
                do {
                    try {
                        $Lineout =  "`r`n"
                        [System.IO.File]::AppendAllText($OutFile,$LineOut,$FileEncoding)
                        $Errorcount = 100
                    } catch {
                        Start-Sleep -Milliseconds 500
                        $Errorcount++
                    }
                } while ($Errorcount -lt 10)
            } 
        } 
    }

    [string]ReplaceSIDs (
        [string]$InputString
    ) {
        $reg = [regex]::new('[S|s]-\d-(?:\d+-){1,14}\d+')
        $hits = $reg.Matches($InputString)
        if ($hits.Value.Count -gt 0) {
            $Values = @($hits.Value.Split(','))
            foreach ($value in $Values) {
                if ($value.Length -gt 12) { # Only convert if SID is longer than 12 characters. All 12 character or less SIDs are well known.
                    # Convert SID into name
                    $objSID = New-Object System.Security.Principal.SecurityIdentifier($value)
                    try {
                        # Added back the 'Domain'-part to security principals, so NT AUTHORITY, NT SERVICE etc can be resolved.
                        $ActualDomain,$ActualName = ($objSID.Translate( [System.Security.Principal.NTAccount] )).Value.Split('\') 
                        # If ActualDomain is the the domain name, replace with 'DOMAIN'
                        if ( ($ActualDomain -eq (Get-ADDomain).DnsRoot) -or ($ActualDomain -eq (Get-ADDomain).NetBIOSName)){
                            # ActualDomain is the DomainFQDN or DomainNB - replace with 'DOMAIN'
                            $ActualDomain = 'DOMAIN'
                        }
                        $InputString = $InputString.Replace($value, "####Replace[$ActualDomain\$ActualName]")
                    } catch {
                        # Do nothing. Was unable to translate SID into name. It probably means it is a well
                        # known SID. In which case, we don't want it to be translated.
                    }
                }
            }
        }
        
        return $InputString
    }

    # The $LowerCase switch is used by policies that require a lower case SID, i.e. s-1-5... as opposed to S-1-5....
    # The folder redirection file fdeploy1.ini always uses lower-case SIDs.
    [string]ReplaceNames (
        [string]$InputString,
        [switch]$LowerCase
    ) {
        if ($InputString -imatch '####Replace') {
            $reg = [regex]::new('####Replace\[(.*?)\]')
            $hits = $reg.Matches($InputString)
            if ($hits.Value.Count -gt 0) {
                $Values = @(($hits.Value.Replace('####Replace','')).Split(','))
                foreach ($value in $Values) {
                    $domain,$usr = ($value.Replace('[', '').Replace(']', '')).split('\')
                    if ($domain -eq 'DOMAIN') {
                        $domain = (Get-ADDomain).NetBIOSName #.Name is incorrect if .Name -ne .NetBIOSName. Can be either .NetBIOSName or .DnsRoot (Domain FQDN)
                    }
                
                    try {
                        $objUser = New-Object System.Security.Principal.NTAccount("$domain", "$usr")
                        $objSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier])
                        
                        if ($objSID) {
                            $SID = $objSID.Value
                            if ($LowerCase) {
                                $SID = $SID.ToLower()
                            }
                            $InputString = $InputString.Replace($value, $SID)
                        } else {
                            # Did not find name in AD.
                            Write-Warning "Did not find '$domain\$usr'. Group policy import will fail if not fixed." -WarningAction Continue
                        }
                    } catch {
                        Write-Warning "Failed to resolve SID for: $domain\$usr" -WarningAction Continue
                        throw $_
                    }
                }
            }
        }
        
        $InputString = $InputString.Replace('####Replace', '')
        return $InputString
    }

    [Object]ConvertPSCustomObjectToHashTable(
        [Object]$PSCustomObject
    ) {
        $Utils = [Utils]::new()
        if ($null -eq $PSCustomObject) { return $null }

        if ($PSCustomObject -is [System.Collections.IEnumerable] -and $PSCustomObject -isnot [string]) {
            $collection = @()
            foreach ($object in $PSCustomObject) { $collection += $Utils.ConvertPSCustomObjectToHashTable($object) }
        } elseif ($PSCustomObject -is [psobject]) {
            $hash = @{}

            foreach ($property in $PSCustomObject.PSObject.Properties) {
                $hash.Add($property.Name, $Utils.ConvertPSCustomObjectToHashTable($property.Value))
            }

            return $hash
        } else { 
            return $PSCustomObject
        }

        return $null
    }

    [Object]ConvertPSCustomObjectToOrderedHashTable(
        [Object]$PSCustomObject
    ) {
        $Utils = [Utils]::new()
        if ($null -eq $PSCustomObject) { return $null }

        if ($PSCustomObject -is [System.Collections.IEnumerable] -and $PSCustomObject -isnot [string]) {
            $collection = @()
            foreach ($object in $PSCustomObject) { $collection += $Utils.ConvertPSCustomObjectToHashTable($object) }
        } elseif ($PSCustomObject -is [psobject]) {
            $hash = [ordered]@{}

            foreach ($property in $PSCustomObject.PSObject.Properties) {
                $hash.Add($property.Name, $Utils.ConvertPSCustomObjectToHashTable($property.Value))
            }

            return $hash
        } else { 
            return $PSCustomObject
        }

        return $null
    }

    [string[]]FormatXml (
        [xml]$Xml,
        [int]$Indent
    ) {
        $StringWriter = New-Object System.IO.StringWriter 
        $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter 
        $xmlWriter.Formatting = "indented" 
        $xmlWriter.Indentation = $Indent
        
        $xml.WriteContentTo($XmlWriter) 
        $XmlWriter.Flush();$StringWriter.Flush() 
        
        $arrFormatedXml = $StringWriter.ToString().Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries)
        return $arrFormatedXml
    }

    [xml]ReplaceXmlValues (
        [xml]$XmlContent,
        [bool]$IsComparing
    ) {
        if ($IsComparing) {
            # These values need to be the same when camparing. Otherwise, the policy will always show as different.
            $ChangedDate = "2020-11-05 12:00:00"
            $newGuid = "E6FBCC5A-59D0-43CC-AD14-FD1143A4ACD8"
        } else {
            $ChangedDate = "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")"
            $newGuid = (New-Guid).ToString().ToUpper()
        }

        # Replace uid
        $nodes = @($XmlContent.SelectNodes("*/*[@uid]"))
        foreach ($node in $nodes) {
            $node.uid = $newGuid
        }
        # Replace changed
        $nodes = @($XmlContent.SelectNodes("*/*[@changed]"))
        foreach ($node in $nodes) {
            $node.changed = $ChangedDate
        }

        # Replace Author
        $nodes = @($XmlContent.SelectNodes(".//Author"))
        foreach ($node in $nodes) {
            $node.'#text' = $env:USERNAME
        }

        return $XmlContent
    }

    [Object]GetScriptPolicyConfig(
        [Object]$Policy,
        [string]$ScriptType,
        [bool]$PowerShell,
        [string]$Scope # Machine, User
    ) {
        $Utils = [Utils]::new()
        $ScriptsToreturn = @()

        $ScriptCmd = [System.String]::Empty
        $Target = $null
        switch ($ScriptType) {
            Startup {
                switch ($PowerShell) {
                    $false {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.ScriptsIni.Startup
                    }
                    $true {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.PSScriptsIni.Startup
                    }
                }
            }
            Shutdown {  
                switch ($PowerShell) {
                    $false {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.ScriptsIni.Shutdown
                    }
                    $true {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.PSScriptsIni.Shutdown
                    }
                }
            }
            Logon {  
                switch ($PowerShell) {
                    $false {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.ScriptsIni.Logon
                    }
                    $true {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.PSScriptsIni.Logon
                    }
                }
            }
            Logoff {  
                switch ($PowerShell) {
                    $false {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.ScriptsIni.Logoff
                    }
                    $true {  
                        $Target = $Policy.PolicySettings.Scripts | Where-Object { $_.Target -eq $Scope }
                        $ScriptCmd = $Target.PSScriptsIni.Logoff
                    }
                }
            }
        }

        # Get all scripts
        $allScripts = $Utils.ConvertPSCustomObjectToOrderedHashTable($ScriptCmd)
        $cmd = [string]::Empty;
        
        $param = [string]::Empty;
        foreach ($item in $allScripts.Keys) {
            if ($item -imatch 'CmdLine') { $cmd = $allScripts[$item] }
            if ($item -imatch 'Parameters') { 
                $param = $allScripts[$item] 

                # Get script content
                $ScriptContent = ($Target.ScriptFiles | Where-Object { $_.Name -eq $cmd }).Content -join "`r`n"

                $ScriptComparer = [ScriptComparer]::new()
                $ScriptComparer.ScriptType = $ScriptType
                $ScriptComparer.ScriptName = $cmd
                $ScriptComparer.ScriptParameters = $param
                $ScriptComparer.IsPowerShell = $PowerShell
                $ScriptComparer.Content = $ScriptContent
                $ScriptsToreturn += $ScriptComparer
            }
        }
        return $ScriptsToReturn
    }

    # The GPO.cmt comment file must contain no more than 2047 carachters, where every new line counts 2 (\r\n)
    [bool]CommentInSpec (
        [array]$InputObject
    ) {
        [int]$threshold = 2047
        [int]$count = 0
        $InputObject.foreach({
            $count += $_.Length
            $count = $count + 2
        })
        $count = $count - 2
        if ($count -gt $threshold) {
            return $false
        }
        else  {
            return $true
        }  
    }
}

class IniSection {
    [string]$SectionName
    [string[]]$SectionContent

    IniSection () { }
}

class Linktarget : BaseSettings {
    [string]$Target
    [int]$LinkOrder
    [bool]$LinkPolicy
    [bool]$LinkEnabled
    [bool]$Enforced

    LinkTarget () {
        $this.LinkPolicy = $false
        $this.LinkEnabled = $false
        $this.Enforced = $false
        $this.ObjectType = "LinkTarget"
    }
    
    LinkTarget (
        [string]$Target,
        [int]$LinkOrder,
        [bool]$LinkPolicy,
        [bool]$LinkEnabled,
        [bool]$Enforced
    ) {
        $this.Target = $Target
        $this.LinkOrder = $Linkorder
        $this.LinkPolicy = $LinkPolicy
        $this.LinkEnabled = $LinkEnabled
        $this.Enforced = $Enforced
        $this.ObjectType = "LinkTarget"
    }
}

class PolicySettings : BaseSettings {
    [RegistrySetting[]]$RegistrySettings
    [AuditSetting[]]$AuditSettings
    [string[]]$SecurityTemplate
    [string[]]$FolderRedirection
    [string[]]$FolderRedirection1
    [string[]]$GPOComments
    [string[]]$MachineComments
    [string[]]$UserComments
    [Script[]]$Scripts
    [GroupPolicyPreference[]]$GroupPolicyPreferences
    
    PolicySettings () {
        $this.ObjectType = "PolicySettings"
    }

    Add (
        [RegistrySetting]$RegistrySetting
    ) {
        $this.RegistrySettings += $RegistrySetting
    }

    Add (
        [AuditSetting]$AuditSetting
    ) {
        $this.AuditSettings += $AuditSetting
    }
}

class WMIFilter : BaseSettings {
    [string]$Name
    [string]$Query
    [string]$Description

    WMIFilter () {
        $this.ObjectType = "WMIFilter"
    }

    WMIFilter (
        [string]$Name,
        [string]$Query,
        [string]$Description
    ) {
        $this.ObjectType = "WMIFilter"
        $this.Name = $Name
        $this.Query = $Query
        $this.Description = $Description
    }
}

class Permission : BaseSettings {
    [string]$Trustee
    [string]$TrusteeType
    [string]$PermissionLevel
    [bool]$Inherited
    [bool]$Replace

    Permission () {
        $this.ObjectType = "Permission"
        $this.Replace = $true
    }

    Permission (
        [string]$Trustee,
        [string]$TrusteeType,
        [string]$PermissionLevel,
        [bool]$Inherited
    ) {
        $this.ObjectType = "Permission"
        $this.Trustee = $Trustee
        $this.TrusteeType = $TrusteeType
        $this.PermissionLevel = $PermissionLevel
        $this.Inherited = $Inherited
        $this.Replace = $true
    }
}

class RegistrySetting : BaseSettings {
    [string]$Target # User, Machine
    [string]$KeyName
    [ValidateSet("REG_SZ","REG_DWORD","REG_QWORD","REG_NONE","REG_MULTI_SZ","REG_BINARY","REG_EXPAND_SZ","REG_DWORD_LITTLE_ENDIAN","REG_DWORD_BIG_ENDIAN","REG_QWORD_LITTLE_ENDIAN")]
    [string]$ValueType
    [string]$ValueName
    [string]$ValueData

    RegistrySetting () {
        $this.ObjectType = "RegistrySetting"
    }

    RegistrySetting (
        [string]$Target,
        [string]$KeyName,
        [string]$ValueType,
        [string]$ValueName,
        [string]$ValueData
    ) {
        $this.ObjectType = "RegistrySetting"
        $this.Target = $Target
        $this.KeyName = $KeyName
        $this.ValueType = $ValueType
        $this.ValueName = $ValueName
        $this.ValueData = $ValueData
        
        # Need to verify if value contains a SID other than well known SIDs
        #$Utils = [Utils]::new()
        #$this.ValueData = $Utils.ReplaceSIDs($ValueData)
    }
}

class AuditSetting : BaseSettings {
    [string]${Machine Name}
    [string]${Policy Target}
    [string]$SubCategory
    [string]${SubCategory GUID}
    [string]${Inclusion Setting}
    [string]${Exclusion Setting}
    [string]${Setting Value}

    AuditSetting() {
        $this.ObjectType = "AuditSetting"
    }

    AuditSetting(
        # Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value
        [string]${Machine Name},
        [string]${Policy Target},
        [string]$SubCategory,
        [string]${SubCategory GUID},
        [string]${Inclusion Setting},
        [string]${Exclusion Setting},
        [string]${Setting Value}
    ) {
        $this.{Machine Name} = ${Machine Name}
        $this.{Policy Target} = ${Policy Target}
        $this.SubCategory = $SubCategory
        $this.{SubCategory GUID} = ${SubCategory GUID}
        $this.{Exclusion Setting} = ${Exclusion Setting}
        $this.{Inclusion Setting} = ${Inclusion Setting}
        $this.{Setting Value} = ${Setting Value}
        $this.ObjectType = "AuditSetting"
    }
}

class SecurityTemplate : BaseSettings {
    [IniSection[]]$GplTemplInf

    SecurityTemplate () {
        $this.ObjectType = "SecurityTemplate"
    }
}

class ScriptFile : BaseSettings {
    [string]$Type  # Logon, Logoff, Startup or Shutdown
    [string]$Name
    [string[]]$Content

    ScriptFile () {}
}

class Script : BaseSettings {
    [string]$Target # User or Machine
    [PSCustomObject]$ScriptsIni
    [PSCustomObject]$PSScriptsIni
    [ScriptFile[]]$ScriptFiles

    Script () {
        $this.ObjectType = "Scripts"
    }
}

class ScriptComparer : BaseSettings {
    [string]$ScriptType
    [string]$ScriptName
    [string]$ScriptParameters
    [string]$IsPowerShell
    [string]$Content

    ScriptComparer () {
        $this.ObjectType = "ScriptComparer"
    }
}

class GroupPolicyPreference : BaseSettings {
    [string]$Target
    [string]$Type
    [string]$XmlFileName
    [string[]]$XmlContent

    GroupPolicyPreference () {
        $this.ObjectType = "GroupPolicyPreference"
    }
}

function Export-GroupPolicyFromAD {
    param (
        [string]$Name,
        [string]$FileName,
        [switch]$IncludeLinks
    )

    $GPO = [GroupPolicy]::new()
    try {
        $GPO.GetPolicyFromAD($Name, $IncludeLinks)
    } catch {
        throw "Failed to get policy from AD - $($_.Exception.Message)"
    }
    
    try {
        $GPO.WritePolicyToJson($Filename)
    } catch {
        throw "Failed to write policy to file - $($_.Exception.Message)"
    }
}

function Test-GroupPolicyExistenceInAD {
    [OutputType([Bool])]
    param (
        [string]$Name,
        [string]$DomainController = $([DomainController]::findone($($(New-Object DirectoryContext("domain",$([Domain]::GetComputerDomain().Name)))),$([ActiveDirectorySite]::GetComputerSite()).Name).Name)
    )
    try {
        Get-GPO -Name $Name -Server $DomainController -ErrorAction 'Stop' | 
        Out-Null
        $true
    } 
    catch {
        $false
    }
}

function Import-GroupPolicyToAD {
    param (
        [string]$Name,
        [string]$FileName,
        [hashtable]$Replacements,
        [switch]$PerformBackup,
        [switch]$OverwriteExistingPolicy,
        [switch]$RemoveLinks,
        [switch]$DoNotLinkGPO,
        [switch]$DefaultPermissions,
        [string]$DomainController = $([DomainController]::findone($($(New-Object DirectoryContext("domain",$([Domain]::GetComputerDomain().Name)))),$([ActiveDirectorySite]::GetComputerSite()).Name).Name)
    )

    $GPO = [GroupPolicy]::new()
    
    try {
        $GPO.GetPolicyFromJson($FileName,$Replacements, $false)
    } catch {
        throw "Failed to read policy from file - $($_.Exception.Message)"
    }

    if ($Name) {
        $GPO.Name = $Name
    }

    if ($DefaultPermissions) {
        $GPO.Permissions=$null
    }

    try {
        $GPO.WritePolicyToAD($OverwriteExistingPolicy, $PerformBackup, $RemoveLinks, $DoNotLinkGPO, $DomainController)
    } catch {
        # If the GPO import failed, a GPO with no, or only some, settings
        # may have been created - ensure that any incomplete GPO is removed
        $GPO.RemoveGroupPolicyFromAD($DomainController)
        throw "Failed to write policy to AD - $($_.Exception.Message)"
    }
}

Export-ModuleMember -function Export-GroupPolicyFromAD,Import-GroupPolicyToAD,Compare-GroupPolicyObjects,Test-GroupPolicyExistenceInAD