Public/New-Tier0FineGrainPasswordPolicy.ps1

function New-Tier0FineGrainPasswordPolicy {

    <#
        .SYNOPSIS
            Creates and configures Fine Grained Password Policies (FGPP) for Tier 0 administrative accounts and service accounts.

        .DESCRIPTION
            Creates two different Fine Grained Password Policies (FGPPs):
            1. A policy for administrative accounts with stricter password requirements.
            2. A policy for service accounts with longer password age settings.

            These policies are then applied to the appropriate security groups and users according to
            the configuration specified in the XML configuration file.

        .PARAMETER ConfigXMLFile
            [System.IO.FileInfo] Full path to the XML configuration file.
            Contains all naming conventions, OU structure, and security settings.
            Must be a valid XML file with required schema elements.
            Default: C:\PsScripts\Config.xml

        .PARAMETER DMScripts
            [System.String] Path to all the scripts and files needed by this function.
            Must contain a 'SecTmpl' subfolder.
            Default: C:\PsScripts\

        .EXAMPLE
            New-Tier0FineGrainPasswordPolicy -ConfigXMLFile C:\PsScripts\Config.xml -Verbose
            Creates Fine Grained Password Policies as defined in the Config.xml file and provides verbose output.

        .EXAMPLE
            New-Tier0FineGrainPasswordPolicy -ConfigXMLFile C:\PsScripts\Config.xml -DMScripts C:\Scripts\DMScripts\
            Creates Fine Grained Password Policies using the Config.xml file and scripts located in the specified path.

        .INPUTS
            System.IO.FileInfo
            System.String

        .OUTPUTS
            None. This function does not generate any output.

        .NOTES
            Used Functions:
                Name ║ Module/Namespace
                ══════════════════════════════════════════╬══════════════════════════════
                Get-ADFineGrainedPasswordPolicy ║ ActiveDirectory
                New-ADFineGrainedPasswordPolicy ║ ActiveDirectory
                Add-ADFineGrainedPasswordPolicySubject ║ ActiveDirectory
                Get-ADGroup ║ ActiveDirectory
                Get-Content ║ Microsoft.PowerShell.Management
                Test-Path ║ Microsoft.PowerShell.Management
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Warning ║ Microsoft.PowerShell.Utility
                Write-Error ║ Microsoft.PowerShell.Utility
                Import-MyModule ║ EguibarIT
                Get-FunctionDisplay ║ EguibarIT

        .NOTES
            Version: 1.1
            DateModified: 22/May/2025
            LastModifiedBy: Vicente Rodriguez Eguibar
                        vicente@eguibar.com
                        Eguibar IT
                        http://www.eguibarit.com

        .LINK
            https://github.com/vreguibar/EguibarIT

        .COMPONENT
            Active Directory

        .ROLE
            Security Administrator

        .FUNCTIONALITY
            Password Policy Management
    #>


    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    [OutputType([System.Void])]

    param (

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false,
            HelpMessage = 'Full path to the configuration.xml file',
            Position = 0)]
        [ValidateScript({
                if (-Not ($_ | Test-Path -PathType Leaf) ) {
                    throw ('File not found: {0}' -f $_)
                }
                if ($_.Extension -ne '.xml') {
                    throw ('File must be XML: {0}' -f $_)
                }
                try {
                    [xml]$xml = Get-Content -Path $_ -ErrorAction Stop
                    # Verify required XML elements are present
                    if ($null -eq $xml.n.Admin -or
                        $null -eq $xml.n.Admin.PSOs -or
                        $null -eq $xml.n.Admin.PSOs.ItAdminsPSO -or
                        $null -eq $xml.n.Admin.PSOs.ServiceAccountsPSO ) {
                        throw 'XML file is missing required elements (Admin, PSOs, ItAdminsPSO or ServiceAccountsPSO section)'
                    }
                    return $true
                } catch {
                    throw ('Invalid XML file: {0}' -f $_.Exception.Message)
                }
            })]
        [PSDefaultValue(Help = 'Default Value is "C:\PsScripts\Config.xml"',
            Value = 'C:\PsScripts\Config.xml'
        )]
        [Alias('Config', 'XML', 'ConfigXml')]
        [System.IO.FileInfo]
        $ConfigXMLFile,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false,
            HelpMessage = 'Path to all the scripts and files needed by this function',
            Position = 1)]
        [PSDefaultValue(
            Help = 'Default Value is "C:\PsScripts\"',
            Value = 'C:\PsScripts\'
        )]
        [Alias('ScriptPath')]
        [string]
        $DMScripts = 'C:\PsScripts\',

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            HelpMessage = 'Start transcript logging to DMScripts path with function name',
            Position = 2)]
        [Alias('Transcript', 'Log')]
        [switch]
        $EnableTranscript

    )

    Begin {
        Set-StrictMode -Version Latest

        If (-not $PSBoundParameters.ContainsKey('ConfigXMLFile')) {
            $PSBoundParameters['ConfigXMLFile'] = 'C:\PsScripts\Config.xml'
        } #end If

        If (-not $PSBoundParameters.ContainsKey('DMScripts')) {
            $PSBoundParameters['DMScripts'] = 'C:\PsScripts\'
        } #end If

        # If EnableTranscript is specified, start a transcript
        if ($EnableTranscript) {
            # Ensure DMScripts directory exists
            if (-not (Test-Path -Path $DMScripts -PathType Container)) {
                try {
                    New-Item -Path $DMScripts -ItemType Directory -Force | Out-Null
                    Write-Verbose -Message ('Created transcript directory: {0}' -f $DMScripts)
                } catch {
                    Write-Warning -Message ('Failed to create transcript directory: {0}' -f $_.Exception.Message)
                } #end try-catch
            } #end if

            # Create transcript filename using function name and current date/time
            $TranscriptFile = Join-Path -Path $DMScripts -ChildPath ('{0}_{1}.LOG' -f $MyInvocation.MyCommand.Name, (Get-Date -Format 'yyyyMMdd_HHmmss'))

            try {
                Start-Transcript -Path $TranscriptFile -Force -ErrorAction Stop
                Write-Verbose -Message ('Transcript started: {0}' -f $TranscriptFile)
            } catch {
                Write-Warning -Message ('Failed to start transcript: {0}' -f $_.Exception.Message)
            } #end try-catch
        } #end if

        # Display function header if variables exist
        if ($null -ne $Variables -and
            $null -ne $Variables.Header) {

            $txt = ($Variables.Header -f
                (Get-Date).ToString('dd/MMM/yyyy'),
                $MyInvocation.Mycommand,
                (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False)
            )
            Write-Verbose -Message $txt
        } #end If

        ##############################
        # Module imports

        Import-MyModule -Name 'ActiveDirectory' -Verbose:$false
        Import-MyModule -Name 'EguibarIT' -Verbose:$false
        Import-MyModule -Name 'EguibarIT.DelegationPS' -Verbose:$false

        ##############################
        # Variables Definition

        # Parameters variable for splatting CMDlets
        [hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase)
        #[System.Collections.ArrayList]$ArrayList = [System.Collections.ArrayList]::New()
        [System.Collections.Generic.List[object]]$ArrayList = [System.Collections.Generic.List[object]]::New()

        # Load the XML configuration file
        try {
            [xml]$confXML = [xml](Get-Content -Path $PSBoundParameters['ConfigXMLFile'] -ErrorAction Stop)
            Write-Debug -Message ('Successfully loaded configuration from {0}' -f $PSBoundParameters['ConfigXMLFile'])
        } catch {
            Write-Error -Message ('Error reading XML file: {0}' -f $_.Exception.Message)
            throw
        } #end Try-Catch

        # Naming conventions hashtable
        $NC = @{
            'sl'    = $confXML.n.NC.LocalDomainGroupPreffix
            'sg'    = $confXML.n.NC.GlobalGroupPreffix
            'su'    = $confXML.n.NC.UniversalGroupPreffix
            'Delim' = $confXML.n.NC.Delimiter
            'T0'    = $confXML.n.NC.AdminAccSufix0
            'T1'    = $confXML.n.NC.AdminAccSufix1
            'T2'    = $confXML.n.NC.AdminAccSufix2
        }

        #region Users Variables
        $AdminName = Get-SafeVariable -Name 'AdminName' -CreateIfNotExist {
            try {
                Get-ADUser -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-500' }
            } catch {
                Write-Debug -Message ('Failed to retrieve Administrator: {0}' -f $_.Exception.Message)
                $null
            }
        }

        $NewAdminExists = Get-SafeVariable -Name 'NewAdminExists' -CreateIfNotExist {
            $newAdminName = $confXML.n.Admin.users.NEWAdmin.Name
            if (-not [string]::IsNullOrEmpty($newAdminName)) {
                Get-AdObjectType -Identity $newAdminName
            } else {
                $null
            }
        }
        #endregion Users Variables

        #region Well-Known groups Variables
        $DomainAdmins = Get-SafeVariable -Name 'DomainAdmins' -CreateIfNotExist {
            try {
                Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-512' }
            } catch {
                Write-Debug -Message ('Failed to retrieve Domain Admins group: {0}' -f $_.Exception.Message)
                $null
            }
        }

        $EnterpriseAdmins = Get-SafeVariable -Name 'EnterpriseAdmins' -CreateIfNotExist {
            try {
                Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-519' }
            } catch {
                Write-Debug -Message ('Failed to retrieve Enterprise Admins group: {0}' -f $_.Exception.Message)
                $null
            }
        }
        #endregion Well-Known groups Variables

        #region Global groups Variables
        $SG_InfraAdmins = Get-SafeVariable -Name 'SG_InfraAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.InfraAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_AdAdmins = Get-SafeVariable -Name 'SG_AdAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.AdAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_GpoAdmins = Get-SafeVariable -Name 'SG_GpoAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.GpoAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_Tier0Admins = Get-SafeVariable -Name 'SG_Tier0Admins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.Tier0Admins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_Tier1Admins = Get-SafeVariable -Name 'SG_Tier1Admins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.Tier1Admins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_Tier2Admins = Get-SafeVariable -Name 'SG_Tier2Admins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.Tier2Admins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_Operations = Get-SafeVariable -Name 'SG_Operations' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Servers.GG.Operations.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_ServerAdmins = Get-SafeVariable -Name 'SG_ServerAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Servers.GG.ServerAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_AllSiteAdmins = Get-SafeVariable -Name 'SG_AllSiteAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.AllSiteAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_AllGALAdmins = Get-SafeVariable -Name 'SG_AllGALAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.AllGalAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_GlobalUserAdmins = Get-SafeVariable -Name 'SG_GlobalUserAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.GlobalUserAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_GlobalPcAdmins = Get-SafeVariable -Name 'SG_GlobalPcAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.GlobalPCAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_GlobalGroupAdmins = Get-SafeVariable -Name 'SG_GlobalGroupAdmins' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.GlobalGroupAdmins.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SG_ServiceDesk = Get-SafeVariable -Name 'SG_ServiceDesk' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.ServiceDesk.Name)
            Get-AdObjectType -Identity $groupName
        }

        # Tier Service Account Groups
        # ToDo: the GetSafeVariable is finding the variable, but variable has old DN. Interim fix filling the variable again
        $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.Tier0ServiceAccount.Name)
        $SG_Tier0ServiceAccount = Get-AdObjectType -Identity $groupName


        # ToDo: the GetSafeVariable is finding the variable, but variable has old DN. Interim fix filling the variable again
        $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.Tier1ServiceAccount.Name)
        $SG_Tier1ServiceAccount = Get-AdObjectType -Identity $groupName

        # ToDo: the GetSafeVariable is finding the variable, but variable has old DN. Interim fix filling the variable again
        $groupName = ('{0}{1}{2}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.GG.Tier2ServiceAccount.Name)
        $SG_Tier2ServiceAccount = Get-AdObjectType -Identity $groupName
        #endregion Global groups Variables

        #region Local groups Variables
        $SL_AdRight = Get-SafeVariable -Name 'SL_AdRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.AdRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_InfraRight = Get-SafeVariable -Name 'SL_InfraRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.InfraRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_DnsAdminRight = Get-SafeVariable -Name 'SL_DnsAdminRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.DnsAdminRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_GpoAdminRight = Get-SafeVariable -Name 'SL_GpoAdminRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.GpoAdminRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_PGM = Get-SafeVariable -Name 'SL_PGM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PGM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_PUM = Get-SafeVariable -Name 'SL_PUM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PUM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_GM = Get-SafeVariable -Name 'SL_GM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.GM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_UM = Get-SafeVariable -Name 'SL_UM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.UM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_PSAM = Get-SafeVariable -Name 'SL_PSAM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PSAM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_PAWM = Get-SafeVariable -Name 'SL_PAWM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PAWM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_PISM = Get-SafeVariable -Name 'SL_PISM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PISM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_SAGM = Get-SafeVariable -Name 'SL_SAGM' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.SAGM.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_DcManagement = Get-SafeVariable -Name 'SL_DcManagement' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.DcManagement.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_TransferFSMOright = Get-SafeVariable -Name 'SL_TransferFSMOright' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.TransferFSMOright.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_PromoteDcRight = Get-SafeVariable -Name 'SL_PromoteDcRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PromoteDcRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_DirReplRight = Get-SafeVariable -Name 'SL_DirReplRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.DirReplRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_SvrOpsRight = Get-SafeVariable -Name 'SL_SvrOpsRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Servers.LG.SvrOpsRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_SvrAdmRight = Get-SafeVariable -Name 'SL_SvrAdmRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Servers.LG.SvrAdmRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_GlobalGroupRight = Get-SafeVariable -Name 'SL_GlobalGroupRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Servers.LG.GlobalGroupRight.Name)
            Get-AdObjectType -Identity $groupName
        }

        $SL_GlobalAppAccUserRight = Get-SafeVariable -Name 'SL_GlobalAppAccUserRight' -CreateIfNotExist {
            $groupName = ('{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Servers.LG.GlobalAppAccUserRight.Name)
            Get-AdObjectType -Identity $groupName
        }
        #endregion Local groups Variables

        # Create collection arrays for convenience
        [array]$AllGlobalGroupVariables = @(
            $DomainAdmins,
            $EnterpriseAdmins,
            $SG_InfraAdmins,
            $SG_AdAdmins,
            $SG_GpoAdmins,
            $SG_Tier0Admins,
            $SG_Tier1Admins,
            $SG_Tier2Admins,
            $SG_Operations,
            $SG_ServerAdmins,
            $SG_AllSiteAdmins,
            $SG_AllGALAdmins,
            $SG_GlobalUserAdmins,
            $SG_GlobalPcAdmins,
            $SG_GlobalGroupAdmins,
            $SG_ServiceDesk
        )

        [array]$AllLocalGroupVariables = @(
            $SL_AdRight,
            $SL_InfraRight,
            $SL_DnsAdminRight,
            $SL_GpoAdminRight,
            $SL_PGM,
            $SL_PUM,
            $SL_GM,
            $SL_UM,
            $SL_PSAM,
            $SL_PAWM,
            $SL_PISM,
            $SL_SAGM,
            $SL_DcManagement,
            $SL_TransferFSMOright,
            $SL_PromoteDcRight,
            $SL_DirReplRight,
            $SL_SvrOpsRight,
            $SL_SvrAdmRight,
            $SL_GlobalGroupRight,
            $SL_GlobalAppAccUserRight
        )

    } #end Begin

    Process {

        if ($PSCmdlet.ShouldProcess('Active Directory', 'Create Tier0 Fine Grain Password Policies')) {

            ###############################################################################
            #region Create a New Fine Grained Password Policy for Admins Accounts

            [string]$PsoName = $confXML.n.Admin.PSOs.ItAdminsPSO.Name
            Write-Verbose -Message ('Processing Admin PSO: {0}' -f $PsoName)

            # Check if the PSO already exists
            $Splat = @{
                Filter      = { name -like $PsoName }
                ErrorAction = 'SilentlyContinue'
            }
            $PSOexists = Get-ADFineGrainedPasswordPolicy @Splat

            if (-not($PSOexists)) {
                Write-Debug -Message ('Creating Admin PSO: {0}' -f $PsoName)

                $Splat = @{
                    Name                        = $confXML.n.Admin.PSOs.ItAdminsPSO.Name
                    Precedence                  = $confXML.n.Admin.PSOs.ItAdminsPSO.Precedence
                    ComplexityEnabled           = [System.Boolean]$confXML.n.Admin.PSOs.ItAdminsPSO.ComplexityEnabled
                    Description                 = $confXML.n.Admin.PSOs.ItAdminsPSO.Description
                    DisplayName                 = $confXML.n.Admin.PSOs.ItAdminsPSO.DisplayName
                    LockoutDuration             = $confXML.n.Admin.PSOs.ItAdminsPSO.LockoutDuration
                    LockoutObservationWindow    = $confXML.n.Admin.PSOs.ItAdminsPSO.LockoutObservationWindow
                    LockoutThreshold            = $confXML.n.Admin.PSOs.ItAdminsPSO.LockoutThreshold
                    MaxPasswordAge              = $confXML.n.Admin.PSOs.ItAdminsPSO.MaxPasswordAge
                    MinPasswordAge              = $confXML.n.Admin.PSOs.ItAdminsPSO.MinPasswordAge
                    MinPasswordLength           = $confXML.n.Admin.PSOs.ItAdminsPSO.MinPasswordLength
                    PasswordHistoryCount        = $confXML.n.Admin.PSOs.ItAdminsPSO.PasswordHistoryCount
                    ReversibleEncryptionEnabled = [System.Boolean]$confXML.n.Admin.PSOs.ItAdminsPSO.ReversibleEncryptionEnabled
                    Passthru                    = $true
                }

                try {

                    $PSOexists = New-ADFineGrainedPasswordPolicy @Splat -ErrorAction Stop
                    Write-Debug -Message ('Created PSO: {0}' -f $PsoName)
                } catch {

                    Write-Error -Message ('Failed to create PSO {0}: {1}' -f $PsoName, $_.Exception.Message)
                    # Refresh the PSOexists variable to get the latest object
                    $Splat = @{
                        Filter      = { name -like $PsoName }
                        ErrorAction = 'SilentlyContinue'
                    }
                    $PSOexists = Get-ADFineGrainedPasswordPolicy @Splat

                } #end Try-Catch

            } else {

                Write-Verbose -Message ('PSO already exists: {0}' -f $PsoName)

            } #end If PSO exists

            # Only proceed if PSO exists
            if ($null -ne $PSOexists) {

                Write-Debug -Message ('Applying PSO {0} to corresponding accounts and groups' -f $PsoName)

                # Allow Active Directory time to process the PSO creation
                Start-Sleep -Seconds 5

                # Apply the PSO to the corresponding accounts and groups
                $ArrayList.Clear()

                # Add Global Groups to ArrayList
                foreach ($Item in $AllGlobalGroupVariables) {
                    if ($null -ne $Item) {
                        [void]$ArrayList.Add($Item)
                    } #end If
                } #end ForEach

                # Add Local Groups to ArrayList
                foreach ($Item in $AllLocalGroupVariables) {
                    if ($null -ne $Item) {
                        [void]$ArrayList.Add($Item)
                    } #end If
                } #end ForEach

                # Only add subjects if there are any
                if ($ArrayList.Count -gt 0) {
                    try {
                        # Process each subject individually to handle errors gracefully
                        foreach ($Subject in $ArrayList) {
                            try {
                                Add-ADFineGrainedPasswordPolicySubject -Identity $PsoName -Subjects $Subject -ErrorAction Stop
                                Write-Debug -Message ('Added group {0} to PSO {1}' -f $Subject.Name, $PsoName)
                            } catch {
                                Write-Warning -Message ('Failed to add group {0} to PSO {1}: {2}' -f
                                    $Subject.Name, $PsoName, $_.Exception.Message)
                            }
                        }
                    } catch {
                        Write-Error -Message ('Failed to add groups to PSO {0}: {1}' -f $PsoName, $_.Exception.Message)
                    } #end Try-Catch
                } else {
                    Write-Debug -Message ('No groups found to add to PSO: {0}' -f $PsoName)
                } #end If ArrayList

                $ArrayList.Clear()

                if ($null -ne $AdminName) {
                    [void]$ArrayList.Add($AdminName)
                } #end if
                if ($null -ne $NewAdminExists) {
                    [void]$ArrayList.Add($NewAdminExists)
                } #end if

                # Only add subjects if there are any
                if ($ArrayList.Count -gt 0) {
                    try {
                        # Process each subject individually to handle errors gracefully
                        foreach ($Subject in $ArrayList) {
                            try {
                                Add-ADFineGrainedPasswordPolicySubject -Identity $PsoName -Subjects $Subject -ErrorAction Stop
                                Write-Debug -Message ('Added user {0} to PSO {1}' -f $Subject.Name, $PsoName)
                            } catch {
                                Write-Warning -Message ('Failed to add user {0} to PSO {1}: {2}' -f
                                    $Subject.Name, $PsoName, $_.Exception.Message)
                            }
                        }
                    } catch {

                        Write-Error -Message ('Failed to add users to PSO {0}: {1}' -f $PsoName, $_.Exception.Message)

                    } #end Try-Catch
                } else {

                    Write-Debug -Message ('No individual users found to add to PSO: {0}' -f $PsoName)

                } #end If ArrayList

            } else {

                Write-Warning -Message ('Could not find or create PSO: {0}' -f $PsoName)

            } #end If PSOexists

            #endregion
            ###############################################################################

            ###############################################################################
            #region Create a New Fine Grained Password Policy for Service Accounts

            [string]$PsoName = $confXML.n.Admin.PSOs.ServiceAccountsPSO.Name
            Write-Verbose -Message ('Processing Service Account PSO: {0}' -f $PsoName)

            # Check if the PSO already exists
            $Splat = @{
                Filter      = { name -like $PsoName }
                ErrorAction = 'SilentlyContinue'
            }
            $PSOexists = Get-ADFineGrainedPasswordPolicy @Splat

            if (-not($PSOexists)) {
                Write-Verbose -Message ('Creating Service Account PSO: {0}' -f $PsoName)

                $Splat = @{
                    Name                        = $confXML.n.Admin.PSOs.ServiceAccountsPSO.Name
                    Precedence                  = $confXML.n.Admin.PSOs.ServiceAccountsPSO.Precedence
                    ComplexityEnabled           = [System.Boolean]$confXML.n.Admin.PSOs.ServiceAccountsPSO.ComplexityEnabled
                    Description                 = $confXML.n.Admin.PSOs.ServiceAccountsPSO.Description
                    DisplayName                 = $confXML.n.Admin.PSOs.ServiceAccountsPSO.DisplayName
                    LockoutDuration             = $confXML.n.Admin.PSOs.ServiceAccountsPSO.LockoutDuration
                    LockoutObservationWindow    = $confXML.n.Admin.PSOs.ServiceAccountsPSO.LockoutObservationWindow
                    LockoutThreshold            = $confXML.n.Admin.PSOs.ServiceAccountsPSO.LockoutThreshold
                    MaxPasswordAge              = $confXML.n.Admin.PSOs.ServiceAccountsPSO.MaxPasswordAge
                    MinPasswordAge              = $confXML.n.Admin.PSOs.ServiceAccountsPSO.MinPasswordAge
                    MinPasswordLength           = $confXML.n.Admin.PSOs.ServiceAccountsPSO.MinPasswordLength
                    PasswordHistoryCount        = $confXML.n.Admin.PSOs.ServiceAccountsPSO.PasswordHistoryCount
                    ReversibleEncryptionEnabled =
                    [System.Boolean]$confXML.n.Admin.PSOs.ServiceAccountsPSO.ReversibleEncryptionEnabled
                    Passthru                    = $true
                }

                try {

                    $PSOexists = New-ADFineGrainedPasswordPolicy @Splat -ErrorAction Stop
                    Write-Debug -Message ('Created PSO: {0}' -f $PsoName)
                } catch {

                    Write-Error -Message ('Failed to create PSO {0}: {1}' -f $PsoName, $_.Exception.Message)
                    # Try to get the PSO if it was created despite the error
                    $Splat = @{
                        Filter      = { name -like $PsoName }
                        ErrorAction = 'SilentlyContinue'
                    }
                    $PSOexists = Get-ADFineGrainedPasswordPolicy @Splat

                } #end Try-Catch

            } else {

                Write-Debug -Message ('PSO already exists: {0}' -f $PsoName)

            } #end If PSO exists

            # Only proceed if PSO exists
            if ($null -ne $PSOexists) {

                Write-Debug -Message ('Applying PSO {0} to corresponding service accounts' -f $PsoName)

                # Allow Active Directory time to process the PSO creation
                Start-Sleep -Seconds 5

                # Apply the PSO to all Tier Service Accounts
                $ArrayList.Clear()
                if ($null -ne $SG_Tier0ServiceAccount) {
                    [void]$ArrayList.Add($SG_Tier0ServiceAccount)
                } #end if
                if ($null -ne $SG_Tier1ServiceAccount) {
                    [void]$ArrayList.Add($SG_Tier1ServiceAccount)
                } #end if
                if ($null -ne $SG_Tier2ServiceAccount) {
                    [void]$ArrayList.Add($SG_Tier2ServiceAccount)
                } #end if

                # Only add subjects if there are any
                if ($ArrayList.Count -gt 0) {
                    try {
                        # Process each subject individually to handle errors gracefully
                        foreach ($Subject in $ArrayList) {
                            try {
                                Add-ADFineGrainedPasswordPolicySubject -Identity $PsoName -Subjects $Subject -ErrorAction Stop
                                Write-Debug -Message ('Added {0} to PSO {1}' -f $Subject.Name, $PsoName)
                            } catch {
                                Write-Warning -Message ('Failed to add {0} to PSO {1}: {2}' -f
                                    $Subject.Name, $PsoName, $_.Exception.Message)
                            }
                        }
                    } catch {
                        Write-Error -Message ('Failed to add service accounts to PSO {0}: {1}' -f
                            $PsoName, $_.Exception.Message)
                    } #end Try-Catch

                } else {
                    Write-Debug -Message ('No service account groups found to add to PSO: {0}' -f $PsoName)
                } #end If ArrayList
            } #end If PSOexists

            #endregion
            ###############################################################################
        } #end If ShouldProcess
    } #end Process

    End {
        # Display function footer if variables exist
        if ($null -ne $Variables -and
            $null -ne $Variables.Footer) {

            $txt = ($Variables.Footer -f $MyInvocation.InvocationName,
                'Create Tier0 Fine Grain Password Policy.'
            )
            Write-Verbose -Message $txt
        } #end If

        # Stop transcript if it was started
        if ($EnableTranscript) {
            try {
                Stop-Transcript -ErrorAction Stop
                Write-Verbose -Message 'Transcript stopped successfully'
            } catch {
                Write-Warning -Message ('Failed to stop transcript: {0}' -f $_.Exception.Message)
            } #end Try-Catch
        } #end If
    } #end End
} #end Function New-Tier0FineGrainPasswordPolicy