ActiveDirectoryTools.psm1

#Try to import these, but silently continue on error since another module
#may use this as a dependency, but doesn't have these installed yet
if ((Get-Module -Name ActiveDirectory) -eq $null)
{
    Import-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
}

if ((Get-Module -Name GroupPolicy) -eq $null)
{
    Import-Module -Name GroupPolicy -ErrorAction SilentlyContinue
}

Function Import-WmiFiltersFromJson {
    <#
        .SYNOPSIS
            Imports WMI Filter information from a JSON file.
 
        .DESCRIPTION
            The cmdlet uses a backup of WMI filters written to a json file to restore each WMI filter in the provided domain.
 
        .PARAMETER Path
            The location of the json file containing the WMI information. This file is created with the Export-GPOBackupsAndWmiFilters cmdlet. It can also be created manually.
 
        .PARAMETER Domain
            The domain to import the WMI filters. This defaults to the domain of the current user.
 
        .PARAMETER Force
            Overwrites any existing WMI filters.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .PARAMETER CimSession
            Specifies the CIM session to use for this cmdlet. Enter a variable that contains the CIM session or a command that creates or gets the CIM session, such as the New-CimSession or Get-CimSession cmdlets. For more information, see about_CimSessions.
 
        .EXAMPLE
            Import-WmiFiltersFromJson -Path "c:\GPOBackups\WmiFilters.json" -Domain "contoso.com"
 
            Imports the WMI filters stored at c:\GPOBackups\WmiFilters.json that were backed up to that path with Export-GPOBackupsAndWmiFilters to contoso.com.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>


    Param (
        [CmdletBinding()]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({ Test-Path -Path $_ })]
        [System.String]$Path,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimSession]$CimSession
    )

    Begin {
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ($CimSession -eq $null)
        {
            $CimSession = New-CimSession
        }

        if ($Domain -eq [System.String]::Empty)
        {
            $Domain = $env:USERDOMAIN
        }

        $ADDomain = Get-ADDOmain -Identity $Domain @CredSplat

        $DomainDN = $ADDomain.DistinguishedName
        $DomainName = $ADDomain.DnsRoot

        $Server = $DomainName

        #If the local system account executing this cmdlet is a domain controller, set the server to this computer
        if ([System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem) 
        {
            $Role = Get-CimInstance -Class Win32_OperatingSystem -Property ProductType -CimSession $CimSession | Select-Object -ExpandProperty ProductType
            
            if ($Role -eq 2) 
            {
                $Server = $env:COMPUTERNAME
            }
        }

        $WmiPath = "CN=SOM,CN=WMIPolicy,CN=System,$DomainDN"

        $WmiFilters = Get-Content -Path $Path -Raw @CredSplat | ConvertFrom-Json

        foreach ($Filter in $WmiFilters)
        {
            $Attr = @{
                "msWMI-Name" = $Filter."msWMI-Name";
                "msWMI-Parm1" = if ($Filter."msWMI-Parm1" -ne $null) { $Filter."msWMI-Param1"} else {$Filter."msWMI-Name"};
                "msWMI-Parm2" = $Filter."msWMI-Parm2";
                "msWMI-Author" = $Filter."msWMI-Author";
                "msWMI-ID"= $Filter."msWMI-ID";
                "instanceType" = 4;
                "showInAdvancedViewOnly" = "TRUE";
                "distinguishedname" = "CN=$($Filter."msWMI-ID"),$WmiPath";
                "msWMI-ChangeDate" = $Filter."msWMI-ChangeDate"; 
                "msWMI-CreationDate" = $Filter."msWMI-CreationDate"
            }

            $ExistingFilter = $null

            try 
            {
                $ExistingFilter = Get-ADObject -Identity $Attr.distinguishedname -Server $Server -ErrorAction Stop @CredSplat
            }
            catch [Exception] 
            {
                Write-Verbose -Message "[ERROR] $($_.Exception.Message)"
            }

            if ($ExistingFilter -ne $null)
            {
                if ($Force)
                {
                    Write-Host -Object "Replacing $($Attr.'msWMI-Name') WMI Filter"
                    Remove-ADObject -Identity $Attr.distinguishedname -Confirm:$false -Server $Server @CredSplat                
                    New-ADObject -Name $Attr."msWMI-ID" -Type "msWMI-Som" -Path $WmiPath -OtherAttributes $Attr -Server $Server @CredSplat
                }
                else
                {
                    Write-Warning -Message "The WMI Filter $($Attr.'msWMI-Name') with this ID already exists."
                }
            }
            else
            {
                Write-Host -Object "Importing $($Attr.'msWMI-Name') WMI Filter"
                New-ADObject -Name $Attr."msWMI-ID" -Type "msWMI-Som" -Path $WmiPath -OtherAttributes $Attr -Server $Server @CredSplat
            }
        }
    }

    End {
    }
}

Function Import-GPOPermissionsFromJson {
    <#
        .SYNOPSIS
            Imports Group Policy object permissions information from a JSON file and applies them to applicable GPOs.
 
        .DESCRIPTION
            The cmdlet uses a backup of GPO permissions written to a json file recreate permissions on exported GPOs.
 
        .PARAMETER Path
            The location of the json file containing the Group Policy permissions. This file is created with the Export-GPOBackupsAndWmiFilters cmdlet. It can also be created manually.
 
        .PARAMETER Domain
            The domain to import the Group Policy object permissions to. This defaults to the domain of the current user.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
            The Set-GPPermission cmdlet used in this function does not accept a Credential parameter, so the user privileges must have permissions to at least perform those functions. Other functions will be executed with the specified credentials.
 
        .EXAMPLE
            Import-GPOPermissionsFromJson -Path "c:\GPOBackups\GPPermissions.json" -Domain "contoso.com"
 
            Imports the Group Policy permissions stored at c:\GPOBackups\GPPermissions.json that were backed up to that path with Export-GPOBackupsAndWmiFilters to contoso.com.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ })]
        [System.String]$Path,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {    
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ([System.String]::IsNullOrEmpty($Domain)) 
        {
            $Domain = (Get-ADDomain -Current LoggedOnUser @CredSplat).DnsRoot
        }

        [String[]]$Drives = Get-PSDrive | Select-Object -ExpandProperty Name

        $TempDrive = "tempdrive"

        if ($Drives.Contains($TempDrive))
        {
            Write-Verbose -Message "An existing PSDrive exists with name $TempDrive, temporarily removing"
            $OldDrive = Get-PSDrive -Name $TempDrive
            Remove-PSDrive -Name $TempDrive
        }

        $Drive = New-PSDrive -Name $TempDrive -Root "" -PSProvider ActiveDirectory -Server $Domain @CredSplat
        Push-Location -Path "$Drive`:\"

        $GPOs = Get-GPO -All -Domain $Domain -Server $Domain

        $Permissions = ConvertFrom-Json -InputObject (Get-Content -Path $Path -Raw @CredSplat)

        foreach ($Permission in ($Permissions | Where-Object {($GPOs | Select-Object -ExpandProperty DisplayName).Contains($_.Name) })) {
            Write-Host -Object "******** Processing $($Permission.Name) ********"
            Write-Host -Object ""

            $GPO = Get-GPO -Name $Permission.Name -Domain $Domain -Server $Domain

            switch ($Permission.Type) 
            {
                "ACL" {
                    Write-Host -Object "Using ACL type permission."
                    $Name = $Permission.Trustee.Name

                    $ADObject = Get-ADObject -Filter {(name -eq $Name) -or (samAccountName -eq $Name)} -Properties * @CredSplat

                    if ($ADObject -ne $null) {
                        $GPOACL = Get-Acl -Path $GPO.Path

                        foreach ($Entry in $Item.Permission) 
                        {
                            [System.DirectoryServices.ActiveDirectoryAccessRule]$NewAce = New-Object -TypeName System.DirectoryServices.ActiveDirectoryAccessRule( `
                                $ADObject.ObjectSID, $Entry.ActiveDirectoryRights, $Entry.AccessControlType, $Entry.InheritanceType)
     
                            $GPOACL.AddAccessRule($NewAce)
                        }
                    
                        # Commit the ACL
                        Set-Acl -Path $GPO.Path -AclObject $GPOACL -Passthru
                    }
                    else {
                        Write-Warning -Message "Could not find an AD Object matching $Name."
                    }

                    break
                }
                "GPPermission" {
                    Write-Host -Object "Using GPPermission type permission."
                    $SidType =[Microsoft.GroupPolicy.SecurityIdentifierType]($Permission.Trustee.SidType)
                    $TrusteeName = $Permission.Trustee.Name

                    try 
                    {
                        switch ($SidType) 
                        {
                            ([Microsoft.GroupPolicy.SecurityIdentifierType]::Group) {
                                if ((Get-ADGroup -Filter {name -eq $TrusteeName}) -ne $null) 
                                {
                                    Set-GPPermission -Guid $GPO.Id -PermissionLevel $Permission.Permission -Replace -TargetType $Permission.Trustee.SidType -TargetName $Permission.Trustee.Name
                                }
                                else 
                                {
                                    Write-Warning -Message "Could not find a group matching $($Permission.Trustee.Name)."
                                }
                                break
                            }
                            ([Microsoft.GroupPolicy.SecurityIdentifierType]::WellKnownGroup) 
                            {
                                Set-GPPermission -Guid $GPO.Id -PermissionLevel $Permission.Permission -Replace -TargetType ([Microsoft.GroupPolicy.Commands.PermissionTrusteeType]::Group) -TargetName $Permission.Trustee.Name
                                break
                            }
                            ([Microsoft.GroupPolicy.SecurityIdentifierType]::User) 
                            {
                                if ((Get-ADUser -Filter {(name -eq $TrusteeName) -or (displayName -eq $TrusteeName) -or (samAccountName -eq $TrusteeName)} @CredSplat) -ne $null) 
                                {
                                    Set-GPPermission -Guid $GPO.Id -PermissionLevel $Permission.Permission -Replace -TargetType $Permission.Trustee.SidType -TargetName $Permission.Trustee.Name
                                }
                                else {
                                    Write-Warning -Message "Could not find a user matching $($Permission.Trustee.Name)."
                                }
                                break
                            }
                            ([Microsoft.GroupPolicy.SecurityIdentifierType]::Computer) 
                            {
                                if ((Get-ADComputer -Filter {(name -eq $TrusteeName) -or (displayName -eq $TrusteeName) -or (samAccountName -eq $TrusteeName) } @CredSplat) -ne $null) 
                                {
                                    Set-GPPermission -Guid $GPO.Id -PermissionLevel $Permission.Permission -Replace -TargetType $Permission.Trustee.SidType -TargetName $Permission.Trustee.Name
                                }
                                else 
                                {
                                    Write-Warning -Message "Could not find a computer matching $TrusteeName."
                                }
                                break
                            }
                            default 
                            {
                                Write-Warning -Message "The trustee type $SidType did not match an expected value of Group, WellKnownGroup, User, or Computer."
                                break
                            }
                        }
                    }
                    catch [Exception] 
                    {
                        Write-Warning -Message "Set-GPPermission`n$($Permission.Name)`n$($Permission.Trustee.Name)`n$($Permission.Permission)`n$($_.Exception.Message)"
                    }
                    
                    break
                }
                default 
                {
                    Write-Warning -Message "The ACL type $($Permission.Type) did not match GPPermission or ACL."
                    break
                }
            }
        }

        Pop-Location

        Remove-PSDrive $Drive

        if ($OldDrive -ne $null)
        {
            Write-Verbose -Message "Recreating original PSDrive"
            New-PSDrive -Name $OldDrive.Name -PSProvider $OldDrive.Provider -Root $OldDrive.Root @CredSplat | Out-Null
            $OldDrive = $null
        }
    }

    End {
    }
}

Function Export-GPOBackupsAndWmiFilters {
    <#
        .SYNOPSIS
            Backups all GPOs in a given domain and writes all WMI Filter information to a JSON file. The permissions for each GPO are also written to a JSON file.
 
        .DESCRIPTION
            The cmdlet uses a native backup of every GPO in the domain. Then it queries all of the WMI filters and backs them up to a JSON files. The JSON file can be read later and used to restore all of the WMI filters.
 
            The permissions on each GPO are also backed up to a separate JSON file, which can be used later to set permissions on GPOs that have already been imported.
 
        .PARAMETER Path
            The directory location to store the backups. This directory will be created if it does not exist.
 
        .PARAMETER Domain
            The domain from which to backup GPOs. This defaults to the domain of the current user.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
             
            Requires the ability to create the folder the backups will be stored in and read Active Directory objects and attributes.
 
        .EXAMPLE
            Export-GPOBackupsAndWmiFilter -Path "c:\GPOBackups" -Domain "contoso.com"
 
            Exports all of the GPO backups and WMI filter information for contoso.com to the specified path.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [System.String]$Path,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process
    {        
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ($Domain -eq [System.String]::Empty)
        {
            $Domain = $env:USERDOMAIN
        }

        $Domain = (Get-ADDomain -Identity $Domain @CredSplat).DnsRoot

        $Server = $Domain

        if (Test-IsEnterpriseOrDomainAdmin -Domain $Domain @CredSplat)
        {
            $GPOs = Get-GPO -All -Domain $Domain

            foreach($GPO in $GPOs)
            {
                $BackupDirectory = ($Path + "\" + $GPO.DisplayName.Replace(",","").Replace(":","").Replace("*",""))
                if (!(Test-Path -Path $BackupDirectory))
                {
                    New-Item -Path $BackupDirectory -ItemType Directory -Force @CredSplat | Out-Null
                }
                
                Backup-GPO -Guid $GPO.Id -Path $BackupDirectory -Comment "Backed up on $(Get-Date)" -Server $Server -Domain $Domain 
            }

            $PermissionsBackupFile = New-Item -Path "$Path\GPPermissions.json" -ItemType File -Force @CredSplat
            Export-GPOPermissions -All -Domain $Domain -Destination $PermissionsBackupFile.FullName @CredSplat
            
            $WmiFilters = Get-ADObject -Filter {objectClass -eq "msWMI-Som"} -Properties "msWMI-Author","msWMI-ChangeDate","msWMI-CreationDate","msWMI-ID","msWMI-Name","msWMI-Parm1","msWMI-Parm2" -Server $Server @CredSplat

            if ($WmiFilters -ne $null -and $WmiFilters.Count -gt 0) 
            {
                Write-Host -Object "Backing up WMI Filters."
                $WmiBackupFile = New-Item -Path "$Path\WmiFilters.json" -ItemType File -Force @CredSplat

                $WmiFilters = $WmiFilters | Select-Object -Property "msWMI-Author","msWMI-ChangeDate","msWMI-CreationDate","msWMI-ID","msWMI-Name","msWMI-Parm1","msWMI-Parm2"

                $Json = ConvertTo-Json -InputObject $WmiFilters 
                $Json = $Json.Replace("null", "`"`"")
                Set-Content -Value $Json -Path $WmiBackupFile -Force @CredSplat

                Write-Host "Completed WMI Filter backup."
            } 
            else 
            {
                Write-Host "No WMI filters in the domain."
            }
        }
        else
        {
            throw [System.UnauthorizedAccessException]("You must be at least a Domain Admin to utilize this cmdlet.")
        }
    }

    End {
    }
}

Function Export-GPOPermissions {
    <#
        .SYNOPSIS
            Exports all of the permission information of GPOs to a JSON file.
 
        .DESCRIPTION
            The cmdlet uses the get Get-GPPermission command to get initial information. Any GPOs with custom permissions have their permissions enumerated via ACL. All of the permissions are written to a JSON file that can be used with Import-GPPermissionsFromJson.
 
        .PARAMETER DisplayNames
            The names of the GPOs whose permissions should be backed up.
 
        .PARAMETER All
            Specifies that all GPOs should have their permissions backed up.
 
        .PARAMETER Domain
            The domain from which to backup GPO permissions in. This defaults to the domain of the current user.
 
        .PARAMETER Destination
            The file that should be created with the permission information. If the file exists, the content will be overwritten.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Export-GPOPermissions -All -Destination c:\GPPermissions.json
 
            Exports all of the GPO permissions to c:\GPPermissions.json
 
        .EXAMPLE
            Export-GPOPermissions -DisplayNames @("Default Domain Policy","Default Domain Controllers Policy") -Destination c:\GPPermissions.json
 
            Exports the permissions for the Default Domain Policy and Default Domain Controllers Policy to c:\GPPermissions.json
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>

    [CmdletBinding(DefaultParameterSetName = "Names")]
    Param(
        [Parameter(Position = 0, ParameterSetName = "Names", Mandatory = $true)]
        [ValidateScript({$_.Count -gt 0})]
        [System.String[]]$DisplayNames,

        [Parameter(Position = 0, ParameterSetName="All", Mandatory = $true)]
        [Switch]$All,

        [Parameter(Position = 1, Mandatory = $true)]
        [System.String]$Destination,

        [Parameter(Position = 2)]
        [System.String]$Domain = [System.String]::Empty,
        
        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Switch]$PassThru
            
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ([System.String]::IsNullOrEmpty($Domain)) 
        {
            $Domain = $env:USERDOMAIN
        }

        $Domain = (Get-ADDomain -Identity $Domain @CredSplat).DnsRoot

        $GPOs = @()

        if ($PSCmdlet.ParameterSetName -eq "All") 
        {
            $GPOs += Get-GPO -All
        }
        else 
        {
            foreach ($Name in $DisplayNames) 
            {
                try 
                {
                    $GPOs += Get-GPO -Name $Name -ErrorAction Stop
                }
                catch [Exception] 
                {
                    Write-Verbose -Message $_.Exception.Message
                }
            }
        }

        [PSCustomObject[]]$ACLs = @()

        [System.String[]]$Drives = Get-PSDrive | Select-Object -ExpandProperty Name

        $TempDrive = "tempdrive"

        if ($Drives.Contains($TempDrive))
        {
            Write-Verbose -Message "An existing PSDrive exists with name $TempDrive, temporarily removing"
            $OldDrive = Get-PSDrive -Name $TempDrive
            Remove-PSDrive -Name $TempDrive
        }

        $Drive = New-PSDrive -Name $TempDrive -Root "" -PSProvider ActiveDirectory -Server $Domain @CredSplat
        Push-Location -Path "$Drive`:\"

        foreach ($GPO in $GPOs) 
        {
            Write-Host -Object "Processing $($GPO.DisplayName) permissions."
            $Permissions = Get-GPPermission -Name $GPO.DisplayName -All 

            foreach ($Permission in $Permissions) 
            {

                $Trustee = [PSCustomObject]@{"Domain" = $Permission.Trustee.Domain; "DSPath" = $Permission.Trustee.DSPath; "Name" = $Permission.Trustee.Name; "Sid" = $Permission.Trustee.Sid; "SidType" = $Permission.Trustee.SidType}

                if ($Permission.Permission -eq [Microsoft.GroupPolicy.GPPermissionType]::GpoCustom) 
                {
                    [System.DirectoryServices.ActiveDirectoryAccessRule[]]$ACL = Get-Acl -Path "$($GPO.Path)" | Select-Object -ExpandProperty Access | Where-Object {$_.IdentityReference.ToString().Split("\\")[-1] -eq $Permission.Trustee.Name }

                    $Rights = @()

                    foreach ($ACE in $ACL) 
                    {
                        $Rights += [PSCustomObject]@{"ActiveDirectoryRights" = $ACE.ActiveDirectoryRights; "InheritanceType" = $ACE.InheritanceType; "AccessControlType" = $ACE.AccessControlType; "ObjectType" = $ACE.ObjectType; "InheritanceFlags" = $ACE.InheritanceFlags}
                    }

                    [PSCustomObject]$Entry = @{"Type" = "ACL";"Name" = $GPO.DisplayName; "Trustee" = $Trustee; "GPODistinguishedName" = $GPO.Path; "Permission" = $Rights; "Denied" = if ($ACE.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny) {$true} else {$false}}
                }
                else 
                {
                    [PSCustomObject]$Entry = @{"Type" = "GPPermission"; "Name" = $GPO.DisplayName; "Trustee" = $Trustee; "GPODistinguishedName" = $GPO.Path; "Permission" = $Permission.Permission; "Denied" = $Permission.Denied}
                }
                
                $ACLs += $Entry            
            }
        }

        Pop-Location

        Remove-PSDrive -Name $Drive

        if ($OldDrive -ne $null)
        {
            Write-Host -Object "Recreating original PSDrive" -ForegroundColor Yellow
            New-PSDrive -Name $OldDrive.Name -PSProvider $OldDrive.Provider -Root $OldDrive.Root @CredSplat | Out-Null
            $OldDrive = $null
        }

        if (![System.String]::IsNullOrEmpty($Destination)) 
        {
            Set-Content -Path $Destination -Value (ConvertTo-Json -InputObject $ACLs -Depth 5) -Force -Confirm:$false @CredSplat
        }

        if ($PassThru)
        {
            Write-Output -InputObject $ACLs
        }
    }

    End {
    }
}

Function Import-FullGPOBackups {
    <#
        .SYNOPSIS
            Restores all most recent GPO backups stored under a given path and reassociates WMI filters to the GPO.
 
        .DESCRIPTION
            The cmdlet recursively searches the provided path for GPO backups. Once a backup is found, it is imported into the provided domain. After the import, the GPO Report XML
            is utilized to identify any WMI filters that were used, and reassociates those WMI filters to the GPO if they exist in the domain. This command is useful for restoring GPOs that
            were backed up natively and restoring them to a new domain after the WMI filters have been imported.
 
        .PARAMETER Path
            The base path to start searching from.
 
        .PARAMETER Domain
            The domain to import the GPOs into. This defaults to the domain of the current user.
 
        .PARAMETER MigrationTablePath
            Optionally specify a migration table to be used to conduct the GPO import.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
     
        .PARAMETER CimSession
            Specifies the CIM session to use for this cmdlet. Enter a variable that contains the CIM session or a command that creates or gets the CIM session, such as the New-CimSession or Get-CimSession cmdlets. For more information, see about_CimSessions.
 
        .EXAMPLE
            Import-FullGPOBackups -Path "c:\GPOBackups" -Domain "contoso.com"
 
            Import all of the backups contained in the folder structure to contoso.com and associate any WMI filters.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/8/2017
 
    #>


    [CmdletBinding()]
    Param
    (
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$Path,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter(Position = 2)]
        [ValidateScript({
            if (![System.String]::IsNullOrEmpty($_)) 
            {
                Test-Path -Path $_
            }
            else
            {
                return $true
            }
        })]
        [System.String]$MigrationTablePath = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimSession]$CimSession
    )

    Begin {
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ($CimSession -eq $null)
        {
            $CimSession = New-CimSession
        }

        if ($Domain -eq [System.String]::Empty)
        {
            $Domain = $env:USERDOMAIN
        }

        $DomainObject = Get-ADDomain -Identity $Domain @CredSplat

        $ADDomain = $DomainObject.DnsRoot
        $Server = $ADDomain
        $DomainDN = $DomainObject.DistinguishedName

        Write-Host -Object "Importing GPOs to $DomainDN."

        $DomainController = [System.String]::Empty

        if ([System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem) 
        {
            $Role = Get-CimInstance -Class Win32_OperatingSystem -Property ProductType @CredSplat | Select-Object -ExpandProperty ProductType
            if ($Role -eq 2) 
            {
                $Server = $env:COMPUTERNAME
            }
        }

        $WmiFilters = Get-ADObject -Filter 'objectClass -eq "msWMI-SOM"' -SearchBase $DomainDN -Properties "msWMI-Author","msWMI-ChangeDate","msWMI-CreationDate","msWMI-ID","msWMI-Name","msWMI-Parm1","msWMI-Parm2" -Server $Server @CredSplat

        if ($WmiFilters -eq $null) 
        {
            $WmiFilters = @()
        }
    
        $Directories = Get-ChildItem -Path $Path -Recurse -Directory | Select-Object -ExpandProperty FullName

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

            if ($Backups -ne $null -and $Backups.Count -gt 0)
            {
                foreach($Backup in $Backups)
                {
                    Write-Host "Importing $($Backup.BackupDirectory)"
                    try
                    {
                        $MigSplat = @{}

                        if (![System.String]::IsNullOrEmpty($MigrationTablePath)) 
                        {
                            $MigSplat["MigrationTable"] = $MigrationTablePath
                        }
                        
                        $Gpo = Import-GPO -Path $Backup.BackupDirectory -BackupGpoName $Backup.DisplayName -CreateIfNeeded -Server $Server -Domain $ADDomain -TargetName $Backup.DisplayName -ErrorAction Stop @MigSplat
                        Write-Host -Object "Successfully imported $($Gpo.DisplayName) to $($Gpo.DomainName)."
                    }
                    catch [Exception] 
                    {
                        Write-Warning -Message $_.Exception.Message
                        Write-Warning -Message ($_.CategoryInfo.Category.ToString())
                        Write-Warning -Message ($_.InvocationInfo | Format-List | Out-String)
                        Write-Warning -Message ($_.ScriptStackTrace | Format-List | Out-String)        
                        break
                    }

                    $Counter = 0

                    while ($true) 
                    {
                        try 
                        {
                            #Make sure the GPO gets created
                            $Temp = Get-ADObject -Identity "CN={$($Gpo.Id)},CN=Policies,CN=System,$DomainDN" -ErrorAction Stop @CredSplat
                            break
                        }
                        catch [Exception] 
                        {
                            Write-Host -Object "Waiting for GPO $($Gpo.DisplayName) : $($Gpo.Id) to finish creation..."
                            Start-Sleep -Seconds 1
                            $Counter++

                            if ($Counter -gt 60) 
                            {
                                Write-Warning -Message "Timeout waiting for the GPO to finish creation."
                                $Gpo = $null
                                break
                            }
                        }
                    }

                    if ($WmiFilters.Count -gt 0) 
                    {
                        [Xml]$GpoReport = $Backup.GenerateReport([Microsoft.GroupPolicy.ReportType]::Xml)
                        $Namespace = New-Object System.Xml.XmlNamespaceManager($GpoReport.NameTable)
                        $Namespace.AddNamespace("ns", $GpoReport.DocumentElement.NamespaceURI)
                        [System.Boolean]$FilterDataAvailable = ($GpoReport.SelectSingleNode("//ns:FilterDataAvailable", $Namespace)).InnerText

                        if ($Gpo -ne $null -and $FilterDataAvailable)
                        {
                            $WmiFilterName = ($GpoReport.SelectSingleNode("//ns:FilterName", $Namespace)).InnerText

                            if (![System.String]::IsNullOrEmpty($WmiFilterName) -and $WmiFilters."msWMI-Name".Contains($WmiFilterName))
                            {
                                $WmiObject = $WmiFilters | Where-Object {$_."msWMI-Name" -eq $WmiFilterName} | Select-Object -First 1
                                if ($WmiObject -ne $null) 
                                {
                                    $WmiFilterId = $WmiObject."msWMI-ID"
                                    Write-Host -Object "Using WMI Filter ID: $WmiFilterId for: `nCN={$($Gpo.Id)},CN=Policies,CN=System,$DomainDN"
                                    
                                    try 
                                    {
                                        Set-ADObject -Identity "CN={$($Gpo.Id)},CN=Policies,CN=System,$DomainDN" -Replace @{gPCWQLFilter = "[$ADDomain;$WmiFilterId;0]"} -Server $Server @CredSplat
                                        Write-Host -Object "Added existing WMI Filter $WmiFilterName to the GPO."
                                    }
                                    catch [Exception] {
                                        Write-Warning -Message $_.Exception.Message
                                        Write-Warning -Message ($_.CategoryInfo.Category.ToString())
                                        Write-Warning -Message ($_.InvocationInfo | Format-List | Out-String)
                                        Write-Warning -Message ($_.ScriptStackTrace | Format-List | Out-String)                            
                                    }
                                }
                                else 
                                {
                                    Write-Warning -Message "Could not find the wmi filter even though the filter list contained a match."
                                }
                            }
                        }
                    }
                    else
                    {
                        Write-Verbose -Message "No WMI filters discovered in $DomainDN."
                    }
                }
            }
            else
            {
                Write-Verbose -Message "No backups discovered at $Directory."
            }
        }
    }

    End {
    }
}

Function Get-WmiFilter {
    <#
        .SYNOPSIS
            Gets WMI Filters in the domain.
 
        .DESCRIPTION
            The Get-WmiFilter cmdlet returns a specific WMI Filter object based on a name or GUID or returns all WMI Filters. Optionally, link information for where each WMI filter is applied can be returned.
 
        .PARAMETER GUID
            The GUID of the WMI Filter to get.
 
        .PARAMETER Domain
            The domain to perform the search on. This defaults to the domain of the current user.
 
        .PARAMETER All
            Returns all WMI filters in the domain.
 
        .PARAMETER Name
            The "msWMI-Name" attribute of the WMI Filter to match.
 
        .PARAMETER IncludeLinkInformation
            If specified, information about where each WMI filter is applied is returned as well.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Get-WmiFilter -All -Domain contoso.com
 
            Gets all of the WMI Filters in contoso.com
 
        .EXAMPLE
            Get-WmiFilter -All -IncludeLinkInformation
             
            Gets all of the WMI filters and their link information for the domain of the current user.
 
        .INPUTS
            System.Guid
             
            System.String
 
        .OUTPUTS
            System.Management.Automation.PSObject[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>


    [CmdletBinding(DefaultParameterSetName = "Name")]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = "Guid")]
        [System.Guid]$Guid,

        [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = "Name")]
        [System.String]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = "All")]
        [Switch]$All,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [Switch]$IncludeLinkInformation,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ([System.String]::IsNullOrEmpty($Domain)) 
        {
            $Domain = $env:USERDOMAIN
        }

        $Domain = (Get-ADDomain -Identity $Domain @CredSplat).DnsRoot

        $WmiFilters = @()

        switch ($PSCmdlet.ParameterSetName) 
        {
            "Guid" {
                [System.String]$Guid = "{" + $Guid.ToString() + "}"
                $Filter = {(objectClass -eq "msWMI-SOM") -and (msWMI-ID -eq $Guid)} 
                break
            }
            "Name" {
                $Filter = {(objectClass -eq "msWMI-SOM") -and (msWMI-Name -eq $Name)} 
                break
            }
            "All" {
                $Filter = {objectClass -eq "msWMI-SOM"} 
                break
            }
            default {
                throw "Could not determine parameter set name."
            }
        }

        [PSObject[]]$WmiFilters = Get-ADObject -Filter $Filter -Properties "msWMI-Author","msWMI-ChangeDate","msWMI-CreationDate","msWMI-ID","msWMI-Name","msWMI-Parm1","msWMI-Parm2","DistinguishedName" -Server $Domain @CredSplat

        if ($IncludeLinkInformation) 
        {
            $GPOs = Get-ADObject -Filter {objectClass -eq "groupPolicyContainer"} -Properties "gPCWQLFilter","displayName","name" @CredSplat | 
                Where-Object {![System.String]::IsNullOrEmpty($_.gPCWQLFilter)} | 
                Select-Object -Property "gPCWQLFilter","displayName","name" 

            foreach ($Filter in $WmiFilters) 
            {            
                Add-Member -InputObject $Filter -MemberType NoteProperty -Name "LinkInformation" -Value @() -TypeName [System.Management.Automation.PSObject[]] -Force    
                        
                $Filter.LinkInformation += ($GPOs | Where-Object {$_.gPCWQLFilter.Split(";")[1] -eq $Filter.'msWMI-ID'} | Select-Object "DisplayName","name")
            }
        }

        Write-Output -InputObject $WmiFilters
    }

    End {        
    }
}

Function New-StandardGPOWmiFilters {
    <#
        .SYNOPSIS
            Creates a standard set of WMI filters.
 
        .DESCRIPTION
            The cmdlet creates a set of WMI filters for each domain in the forest, or a particular domain. Some of the software filters use custom WMI classes Win32_Software and Win32_Software64 that can be installed through the Set-Win32Software script on PowerShellGallery.
 
        .PARAMETER Domain
            The domain to create the WMI filters in. This defaults to the domain of the current user.
 
        .PARAMETER WithReplace
            If a WMI filters exists with the same name as ones in the standard set, the WMI query expression is updated.
 
        .PARAMETER AddToForest
            Adds the WMI filters to each domain in the forest of the current user.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
            If the AddToForest parameter is specified, the credentials must have Enterprise Admin rights, otherwise they need Domain Admin rights for the specified domain.
             
        .EXAMPLE
            New-StandardGPOWmiFilters -AddToForest
 
            Creates standard WMI Filters in every domain in the current forest. The user must be an enterprise admin for this command to succeed.
 
        .EXAMPLE
            New-StandardGPOWmiFilters -Domain contoso.com -WithReplace
 
            Creates standard WMI Filters in the contoso.com domain and replaces the query value for any existing filters that have a matching name as a new one being created through the cmdlet.
 
        .EXAMPLE
            New-StandardGPOWmiFilters
 
            Creates standard WMI Filters in the current domain of the user. The user must be a domain admin for this command to succeed.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.Collections.Hashtable[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding(DefaultParameterSetName="SpecifyDomain")]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = "Forest")]
        [Switch]$AddToForest,

        [Parameter(Position = 0, ParameterSetName = "SpecifyDomain", ValueFromPipeLine = $true)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [Switch]$WithReplace,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin{        
    }

    Process
    {
        $Domains = @()

        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        switch ($PSCmdlet.ParameterSetName) 
        {
            "Forest" {
                if (!(Test-IsEnterpriseAdmin @CredSplat)) 
                {
                    throw "The user must be an enterprise admin to create filters in every domain."
                }
                else 
                {
                    $Domains += (Get-ADForest -Current LoggedOnUser @CredSplat).Domains
                }
                break
            }
            "SpecifyDomain" {            
                if ([System.String]::IsNullOrEmpty($Domain)) 
                {
                    $Domain = Get-ADDomain -Current LoggedOnUser @CredSplat | Select-Object -ExpandProperty DnsRoot
                }

                if (!(Test-IsDomainAdmin -Domain $Domain @CredSplat)) 
                {
                    throw "The user must be a domain admin to create filters in the specified domain."
                }
                else 
                {
                    $Domains += $Domain
                }

                break
            }
            default {
                throw "Could not determine parameter set."
            }
        }
        $Output = @()

#region Filters
        $Filters = @()

#region AD and NON AD Management Workstations

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=1
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE ProductType=1
"@

        $ADManagementWorkstation=@{"Expression"=@($Expression,$Expression2);"Description"="Active Directory Management Workstation (Item Level Targetting)";"Name"="Active Directory Management Workstation (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $ADManagementWorkstation

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=0
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE ProductType=1
"@

        $NonADManagementWorkstation=@{"Expression"=@($Expression,$Expression2);"Description"="Non Active Directory Management Workstation (Item Level Targetting)";"Name"="Non Active Directory Management Workstation (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $NonADManagementWorkstation

        $ADMgmtWorkstations = @(
            @{"Version" = "6.0."; "Name" = "Windows 7"},
            @{"Version" = "6.1."; "Name" = "Windows Vista"},
            @{"Version" = "6.2."; "Name" = "Windows 8"},
            @{"Version" = "6.3."; "Name" = "Windows 8.1"},
            @{"Version" = "10.0"; "Name" = "Windows 10"}
        )

        foreach ($Item in $ADMgmtWorkstations) {

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=1
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "$($Item.Version)%" AND ProductType=1
"@

        $ADMgmt=@{"Expression"=@($Expression,$Expression2);"Description"="$($Item.Name) - Active Directory Management (Item Level Targetting)";"Name"="$($Item.Name) - Active Directory Management (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $ADMgmt

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=0
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "$($Item.Version)%" AND ProductType=1
"@

        $NonADMgmt=@{"Expression"=@($Expression,$Expression2);"Description"="$($Item.Name) - Non Active Directory Management (Item Level Targetting)";"Name"="$($Item.Name) - Non Active Directory Management (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $NonADMgmt

        }

#endregion

#region AD and NON AD Management Servers

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=1
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE (ProductType=2 OR ProductType=3)
"@

        $ADManagementServer=@{"Expression"=@($Expression,$Expression2);"Description"="Active Directory Management Server (Item Level Targetting)";"Name"="Active Directory Management Server (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $ADManagementServer

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=0
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE (ProductType=2 OR ProductType=3)
"@

        $NonADManagementServer=@{"Expression"=@($Expression,$Expression2);"Description"="Non Active Directory Management Server (Item Level Targetting)";"Name"="Non Active Directory Management Server (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $NonADManagementServer

        $ADMgmtServers = @(
            @{"Version" = "6.0."; "Name" = "Windows Server 2008"},
            @{"Version" = "6.1."; "Name" = "Windows Server 2008 R2"},
            @{"Version" = "6.2."; "Name" = "Windows Server 2012"},
            @{"Version" = "6.3."; "Name" = "Windows Server 2012 R2"},
            @{"Version" = "10.0"; "Name" = "Windows Server 2016"}
        )

        foreach ($Item in $ADMgmtServers) {
        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=1
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "$($Item.Version)%" AND ProductType=3
"@

        $ADMgmt=@{"Expression"=@($Expression,$Expression2);"Description"="$($Item.Name) - Active Directory Management (Item Level Targetting)";"Name"="$($Item.Name) - Active Directory Management (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $ADMgmt

        $Expression = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue=0
"@

        $Expression2 = @"
SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "$($Item.Version)%" AND ProductType=3
"@

        $NonADMgmt=@{"Expression"=@($Expression,$Expression2);"Description"="$($Item.Name) - Non Active Directory Management (Item Level Targetting)";"Name"="$($Item.Name) - Non Active Directory Management (Item Level Targetting)";"Namespace"="ROOT\CIMV2";}
        $Filters += $NonADMgmt

        }

#endregion

#region Web Browsers

        $Expression = @"
SELECT path,filename,extension,version FROM CIM_DataFile where path="\\Program Files\\Internet Explorer\\" AND filename="iexplore" AND extension="exe" AND version>="10" AND version<"11"
"@

        $IE10 = @{"Expression"=@($Expression);"Description"="Internet Explorer 10";"Name"="Internet Explorer 10";"Namespace"="ROOT\CIMV2";}
        $Filters += $IE10

        $Expression = @"
SELECT path,filename,extension,version FROM CIM_DataFile where path="\\Program Files\\Internet Explorer\\" AND filename="iexplore" AND extension="exe" AND version>="11" AND version<"12"
"@

        $IE11 = @{"Expression"=@($Expression);"Description"="Internet Explorer 11";"Name"="Internet Explorer 11";"Namespace"="ROOT\CIMV2";}
        $Filters += $IE11

        $Expression = @"
SELECT * FROM win32_software WHERE DisplayName = "Google Chrome"
"@

        $GoogleChrome = @{"Expression"=@($Expression);"Description"="Google Chrome";"Name"="Google Chrome";"Namespace"="ROOT\CIMV2";}
        $Filters += $GoogleChrome

        $Expression = @"
SELECT * from win32_software where DisplayName like "%Firefox%"
"@

        $MozillaFirefox = @{"Expression"=@($Expression);"Description"="Mozilla Firefox";"Name"="Mozilla Firefox";"Namespace"="ROOT\CIMV2";}
        $Filters += $MozillaFirefox

#endregion

#region Operating Systems
    
        $Expression = @"
SELECT * FROM Win32_OperatingSystem WHERE ProductType = "2" OR ProductType = "3"
"@

        $WindowsServer = @{"Expression"=@($Expression);"Description"="Windows Server";"Name"="Windows Server";"Namespace"="ROOT\CIMV2";}
        $Filters += $WindowsServer

        $Expression = @"
SELECT * FROM Win32_OperatingSystem WHERE ProductType = "3"
"@

        $MemberServer = @{"Expression"=@($Expression);"Description"="Member Server";"Name"="Member Server";"Namespace"="ROOT\CIMV2";}
        $Filters += $MemberServer
        
        $Expression = @"
SELECT * FROM Win32_OperatingSystem WHERE ProductType = "2"
"@

        $DomainController = @{"Expression"=@($Expression);"Description"="Domain Controller";"Name"="Domain Controller";"Namespace"="ROOT\CIMV2";}
        $Filters += $DomainController

        $Expression = @"
SELECT * FROM Win32_OperatingSystem WHERE ProductType = "1"
"@

        $WindowsClient = @{"Expression"=@($Expression);"Description"="Windows Client";"Name"="Windows Client";"Namespace"="ROOT\CIMV2";}
        $Filters += $WindowsClient

        $Workstations = @(
            @{"Version" = "6.0."; "Name" = "Windows 7"},
            @{"Version" = "6.1."; "Name" = "Windows Vista"},
            @{"Version" = "6.2."; "Name" = "Windows 8"},
            @{"Version" = "6.3."; "Name" = "Windows 8.1"},
            @{"Version" = "10.0"; "Name" = "Windows 10"}
        )

        foreach ($Item in $Workstations) {
            $Expression = @"
SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "$($Item.Version)%" AND ProductType = "1"
"@

            $Workstation = @{"Expression"=@($Expression);"Description"="$($Item.Name)";"Name"="$($Item.Name)";"Namespace"="ROOT\CIMV2";}
            $Filters += $Workstation
        }

        $Servers = @(
            @{"Version" = "6.0."; "Name" = "Windows Server 2008"; "ProductType" = "(ProductType = `"3`" OR ProductType = `"2`")" },
            @{"Version" = "6.0."; "Name" = "Windows Server 2008 Member Server"; "ProductType" = "ProductType = `"3`""},
            @{"Version" = "6.0."; "Name" = "Windows Server 2008 Domain Controller"; "ProductType" = "ProductType = `"2`""},
            @{"Version" = "6.1."; "Name" = "Windows Server 2008 R2"; "ProductType" = "(ProductType = `"3`" OR ProductType = `"2`")" },
            @{"Version" = "6.1."; "Name" = "Windows Server 2008 R2 Member Server"; "ProductType" = "ProductType = `"3`""},
            @{"Version" = "6.1."; "Name" = "Windows Server 2008 R2 Domain Controller"; "ProductType" = "ProductType = `"2`""},
            @{"Version" = "6.2."; "Name" = "Windows Server 2012"; "ProductType" = "(ProductType = `"3`" OR ProductType = `"2`")" },
            @{"Version" = "6.2."; "Name" = "Windows Server 2012 Member Server"; "ProductType" = "ProductType = `"3`""},
            @{"Version" = "6.2."; "Name" = "Windows Server 2012 Domain Controller"; "ProductType" = "ProductType = `"2`""},
            @{"Version" = "6.3."; "Name" = "Windows Server 2012 R2"; "ProductType" = "(ProductType = `"3`" OR ProductType = `"2`")" },
            @{"Version" = "6.3."; "Name" = "Windows Server 2012 R2 Member Server"; "ProductType" = "ProductType = `"3`""},
            @{"Version" = "6.3."; "Name" = "Windows Server 2012 R2 Domain Controller"; "ProductType" = "ProductType = `"2`""},
            @{"Version" = "10.0"; "Name" = "Windows Server 2016"; "ProductType" = "(ProductType = `"3`" OR ProductType = `"2`")" },
            @{"Version" = "10.0"; "Name" = "Windows Server 2016 Member Server"; "ProductType" = "ProductType = `"3`""},
            @{"Version" = "10.0"; "Name" = "Windows Server 2016 Domain Controller"; "ProductType" = "ProductType = `"2`""}
        )

        foreach ($Item in $Servers) {
            $Expression = @"
SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "$($Item.Version)%" AND $($Item.ProductType)
"@

            $Server = @{"Expression"=@($Expression);"Description"="$($Item.Name)";"Name"="$($Item.Name)";"Namespace"="ROOT\CIMV2";}
            $Filters += $Server
        }

        #endregion

#region Microsoft Office
        
        $Office = @(
            @{"Name" = "Access"; "File" = "MSACCESS"},
            @{"Name" = "Excel"; "File" = "EXCEL"},
            @{"Name" = "Groove"; "File" = "GROOVE"},
            @{"Name" = "Infopath"; "File" = "INFOPATH"},
            @{"Name" = "Lync"; "File" = "lync"},
            @{"Name" = "OneNote"; "File" = "ONENOTE"},
            @{"Name" = "Outlook"; "File" = "OUTLOOK"},
            @{"Name" = "Powerpoint"; "File" = "POWERPNT"},
            @{"Name" = "Project"; "File" = "WINPROJ"},
            @{"Name" = "Publisher"; "File" = "MSPUB"},
            @{"Name" = "Sharepoint Designer"; "File" = "SPDESIGN"},
            @{"Name" = "Visio"; "File" = "VISION"},
            @{"Name" = "Word"; "File" = "WINWORD"}
        )

        foreach ($Item in $Office) {
            $Expression = @"
SELECT path,filename,extension,version FROM CIM_DataFile where path="\\Program Files (x86)\\Microsoft Office\\Office15\\" AND filename="$($Item.File)" AND extension="exe" AND version>="15" AND version<"16"
"@

            $Office2013x86 = @{"Expression"=@($Expression);"Description"="Microsoft Office 2013 x86 - $($Item.Name)";"Name"="Microsoft Office 2013 x86 - $($Item.Name)";"Namespace"="ROOT\CIMV2";}
            $Filters += $Office2013x86

            $Expression = @"
SELECT path,filename,extension,version FROM CIM_DataFile where path="\\Program Files\\Microsoft Office\\Office15\\" AND filename="$($Item.File)" AND extension="exe" AND version>="15" AND version<"16"
"@

            $Office2013x64 = @{"Expression"=@($Expression);"Description"="Microsoft Office 2013 x64 - $($Item.Name)";"Name"="Microsoft Office 2013 x64 - $($Item.Name)";"Namespace"="ROOT\CIMV2";}
            $Filters += $Office2013x64

            $Expression = @"
SELECT path,filename,extension,version FROM CIM_DataFile where path="\\Program Files (x86)\\Microsoft Office\\Office16\\" AND filename="$($Item.File)" AND extension="exe" AND version>="16" AND version<"17"
"@

            $Office2016x86 = @{"Expression"=@($Expression);"Description"="Microsoft Office 2016 x86 - $($Item.Name)";"Name"="Microsoft Office 2016 x86 - $($Item.Name)";"Namespace"="ROOT\CIMV2";}
            $Filters += $Office2016x86

            $Expression = @"
SELECT path,filename,extension,version FROM CIM_DataFile where path="\\Program Files\\Microsoft Office\\Office16\\" AND filename="$($Item.File)" AND extension="exe" AND version>="16" AND version<"17"
"@

            $Office2016x64 = @{"Expression"=@($Expression);"Description"="Microsoft Office 2016 x64 - $($Item.Name)";"Name"="Microsoft Office 2016 x64 - $($Item.Name)";"Namespace"="ROOT\CIMV2";}
            $Filters += $Office2016x64

        }

        $Expression = @"
SELECT * from Win32_Software where displayname like "Microsoft Office%2013%"
"@

        $Office2013x86System = @{"Expression"=@($Expression);"Description"="Microsoft Office 2013 x86 - System";"Name"="Microsoft Office 2013 x86 - System";"Namespace"="ROOT\CIMV2";}
        $Filters += $Office2013x86System

        $Expression = @"
SELECT * from Win32_Software64 where displayname like "Microsoft Office%2013%"
"@

        $Office2013x64System = @{"Expression"=@($Expression);"Description"="Microsoft Office 2013 x64 - System";"Name"="Microsoft Office 2013 x64 - System";"Namespace"="ROOT\CIMV2";}
        $Filters += $Office2013x64System

        $Expression = @"
SELECT * from Win32_Software where displayname like "Microsoft Office%2016%"
"@

        $Office2016x86System = @{"Expression"=@($Expression);"Description"="Microsoft Office 2016 x86 - System";"Name"="Microsoft Office 2016 x86 - System";"Namespace"="ROOT\CIMV2";}
        $Filters += $Office2016x86System

        $Expression = @"
SELECT * from Win32_Software64 where displayname like "Microsoft Office%2016%"
"@

        $Office2016x64System = @{"Expression"=@($Expression);"Description"="Microsoft Office 2016 x64 - System";"Name"="Microsoft Office 2016 x64 - System";"Namespace"="ROOT\CIMV2";}
        $Filters += $Office2016x64System

        #endregion

#region Server Features

        $Expression = @"
SELECT * FROM Win32_OptionalFeature Where (Name = "IIS-WebServerRole" OR Name = "IIS-WebServer") AND InstallState = 1
"@

        $IIS = @{"Expression"=@($Expression);"Description"="IIS";"Name"="IIS";"Namespace"="ROOT\CIMV2";}    
        $Filters += $IIS

        $Expression = @"
SELECT * FROM Win32_OptionalFeature WHERE (Name = "DNS-Server-Tools" OR Name = "RSAT-AD-Tools-Feature" OR Name = "RSAT-ADDS-Tools-Feature" OR Name = "DirectoryServices-DomainController-Tools" OR Name = "DirectorySerrvices-AdministrativeCenter") AND InstallState = 1
"@

        $ADManagementSystem = @{"Expression"=@($Expression);"Description"="Active Directory Management System - Feature Query";"Name"="Active Directory Management System - Feature Query";"Namespace"="ROOT\CIMV2";}    
        $Filters += $ADManagementSystem

        $Expression = @"
SELECT * FROM Win32_Service WHERE Name = "WMSvc"
"@

        $WebManagementService=@{"Expression"=@($Expression);"Description"="IIS Web Management Service";"Name"="IIS Web Management Service";"Namespace"="ROOT\CIMV2";}
        $Filters += $WebManagementService

        $Expression = @"
SELECT * FROM Win32_ComputerSystem where DomainRole = 5
"@

        $PDC = @{"Expression"=@($Expression);"Description"="PDC Emulator";"Name"="PDC Emulator";"Namespace"="ROOT\CIMV2";}
        $Filters += $PDC

        $Expression = @"
SELECT * FROM win32_optionalfeature WHERE Name = "FailoverCluster-FullServer" AND InstallState = 1
"@

        $WSFC = @{"Expression"=@($Expression);"Description"="Windows Server Failover Cluster Feature";"Name"="Windows Server Failover Cluster Feature";"Namespace"="ROOT\CIMV2";}
        $Filters += $WSFC

        $Expression = @"
SELECT * FROM win32_optionalfeature WHERE Name = "FailoverCluster-FullServer" AND InstallState = 1
"@

        $Expression2 = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue = 1
"@


        $WSFCAD = @{"Expression"=@($Expression,$Expression2);"Description"="Windows Server Failover Cluster Feature AD";"Name"="Windows Server Failover Cluster Feature AD";"Namespace"="ROOT\CIMV2";}
        $Filters += $WSFCAD

        $Expression = @"
SELECT * FROM win32_optionalfeature WHERE Name = "FailoverCluster-FullServer" AND InstallState = 1
"@

        $Expression2 = @"
SELECT * FROM Win32_Environment WHERE Name = "isADManagementSystem" and VariableValue = 0
"@


        $WSFCNonAD = @{"Expression"=@($Expression,$Expression2);"Description"="Windows Server Failover Cluster Feature Non AD";"Name"="Windows Server Failover Cluster Feature Non AD";"Namespace"="ROOT\CIMV2";}
        $Filters += $WSFCNonAD

        $Expression = @"
SELECT * from win32_optionalfeature where name = "CertificateServices" AND installstate = 1
"@

        $CA = @{"Expression"=@($Expression);"Description"="Active Directory Certificate Services";"Name"="Active Directory Certificate Services";"Namespace"="ROOT\CIMV2";}
        $Filters += $CA

        $Expression = @"
SELECT * from win32_optionalfeature WHERE (name = "WebEnrollmentServices" AND installstate = 1)
"@

        $CAWebEnrollment = @{"Expression"=@($Expression);"Description"="Active Directory Certificate Services - CA Web Enrollment";"Name"="Active Directory Certificate Services - CA Web Enrollment";"Namespace"="ROOT\CIMV2";}
        $Filters += $CAWebEnrollment

#endregion

#region Software

        $Expression = @"
select * from win32_software64 where DisplayName like "%Java%7%" AND VersionMajor = 7
"@

        $JRE7x64 = @{"Expression"=@($Expression);"Description"="JRE 7 x64";"Name"="JRE 7 x64";"Namespace"="ROOT\CIMV2";}    
        $Filters += $JRE7x64

$Expression = @"
select * from win32_software where DisplayName like "%Java%7%" AND VersionMajor = 7
"@

        $JRE7x86 = @{"Expression"=@($Expression);"Description"="JRE 7 x86";"Name"="JRE 7 x86";"Namespace"="ROOT\CIMV2";}    
        $Filters += $JRE7x86

        $Expression = @"
select * from win32_software64 where DisplayName like "%Java%8%" AND VersionMajor = 8
"@

        $JRE8x64 = @{"Expression"=@($Expression);"Description"="JRE 8 x64";"Name"="JRE 8 x64";"Namespace"="ROOT\CIMV2";}    
        $Filters += $JRE8x64

$Expression = @"
select * from win32_software where DisplayName like "%Java%8%" AND VersionMajor = 8
"@

        $JRE8x86 = @{"Expression"=@($Expression);"Description"="JRE 8 x86";"Name"="JRE 8 x86";"Namespace"="ROOT\CIMV2";}    
        $Filters += $JRE8x86

#endregion

#endregion

        foreach ($Domain in $Domains) 
        {
            foreach ($Filter in $Filters)
            {
                Write-Verbose -Message "Adding filter $($Filter.Name) with Expression $($Filter.Expression)"
                $Output += New-GPOWmiFilter -Name $Filter.Name -Description $Filter.Description -Expression $Filter.Expression -Domain $Domain -Namespace $Filter.Namespace -PassThru -WithReplace:$WithReplace @CredSplat
            }
        }

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

    End {
    }
}

Function New-GPOWmiFilter {
    <#
        .SYNOPSIS
            Creates a new WMI filter.
 
        .DESCRIPTION
            The cmdlet takes in the WMI filter properties and produces a usable WMI filter.
 
        .PARAMETER Name
            The WMI filter name.
 
        .PARAMETER Expression
            The expressions to evaluate in the filter, can be multiple WMI queries for a single filter.
 
        .PARAMETER Namespace
            A dynamically generated parameter of the available WMI namespaces. Defaults to ROOT\CIMV2.
 
        .PARAMETER Description
            The WMI filter description. This defaults to the WMI Filter Name.
 
        .PARAMETER Domain
            The domain the WMI filter is being created in, defaults to the domain of the logged on user.
 
        .PARAMETER Author
            The author creating the WMI filter, defaults to the current user.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .PARAMETER WithReplace
            If a WMI filter exists with the same name, the WMI query expression is updated.
 
        .PARAMETER PassThru
            Determines whether to return the new WMI filter object.
 
        .PARAMETER CimSession
            Specifies the CIM session to use for this cmdlet. Enter a variable that contains the CIM session or a command that creates or gets the CIM session, such as the New-CimSession or Get-CimSession cmdlets. For more information, see about_CimSessions.
 
        .EXAMPLE
            New-GPOWmiFilter -Name "Windows Server 2012 R2" -Expression @("Select * From Win32_OperatingSystem Where Version like '6.3%' and ProductType != 1")
 
            Creates a WMI filter to target Windows Server 2012 R2.
 
        .INPUTS
            System.Object
 
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>

    [CmdletBinding(DefaultParameterSetName="Default")]
    Param
    (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Default")]
        [System.String]$Name,

        [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "Default")]
        [System.String[]]$Expression,

        [Parameter(Mandatory = $false, Position = 3, ParameterSetName = "Default")]
        [System.String]$Description = [System.String]::Empty,

        [Parameter(Mandatory = $false, Position = 4, ParameterSetName = "Default")]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter(Mandatory=$false, Position = 5, ParameterSetName = "Default")]
        [System.String]$Author = [System.String]::Empty,

        [Parameter()]
        [Switch]$WithReplace,

        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = "InputObject")]
        [System.Object]$InputObject,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]$PassThru,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimSession]$CimSession
    )

    DynamicParam 
    {
        # Set the dynamic parameters' name
        $ParameterName = "Namespace"
            
        # Create the dictionary
        $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create the collection of attributes
        $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
            
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $false
        $ParameterAttribute.Position = 2
        $ParameterAttribute.ParameterSetName ="Default"

        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)

        # Generate and set the ValidateSet
        $ArraySet = Get-CimInstance -ClassName __Namespace -Namespace "ROOT" | Select-Object -ExpandProperty Name | ForEach-Object {return "ROOT\" + $_} 
        $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($ArraySet)

        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)

        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
        return $RuntimeParameterDictionary
    }

    Begin {
    }

    Process
    {        
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ($CimSession -eq $null)
        {
            $CimSession = New-CimSession
        }

        if ($PSCmdlet.ParameterSetName -eq "InputObject")
        {
            $Author = $InputObject.Author
            $Expression = $InputObject.Expression
            $Domain = $InputObject.Domain
            $Namespace = $InputObject.Namespace
            $Name = $InputObject.Name
            $Description = $InputObject.Description
        }

        if ($Expression.Count -eq 0)
        {
            throw [System.ArgumentException]("At least one Expression Method is required to create a WMI Filter.")
        }

        if ([System.String]::IsNullOrEmpty($Domain))
        {
            $Domain = $env:USERDOMAIN
        }

        [Microsoft.ActiveDirectory.Management.ADDomain]$ADDomain = Get-ADDomain -Identity $Domain @CredSplat

        $DefaultNamingContext = $ADDomain.DistinguishedName

        $Server = [System.String]::Empty

        if ([System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem) 
        {
            $Role = Get-CimInstance -Class Win32_OperatingSystem -Property ProductType -CimSession $CimSession | Select-Object -ExpandProperty ProductType

            if ($Role -eq 2) 
            {
                $Server = $env:COMPUTERNAME
            }
        }

        if ([System.String]::IsNullOrEmpty($Server)) 
        {
            $ContextType = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain
            $Context = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext($ContextType, $ADDomain.DnsRoot)
            $Server = ([System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($Context)).Name
        }

        if ([System.String]::IsNullOrEmpty($PSBoundParameters.Namespace))
        {
            $Namespace = "ROOT\CIMv2"
        }
        else {
            $Namespace = $PSBoundParameters.Namespace
        }
 
        if (![System.String]::IsNullOrEmpty($Author))
        {
            $msWMIAuthor = $Author
        }
        else
        {
            try 
            {
                $User = Get-ADUser $env:USERNAME -ErrorAction SilentlyContinue @CredSplat

                if ($User -ne $null)
                {
                    if (![System.String]::IsNullOrEmpty($User.UserPrincipalName)) 
                    {
                        $msWMIAuthor = $User.UserPrincipalName
                    }
                    else 
                    {
                        $msWMIAuthor = $User.SamAccountName
                    }
                }
                else
                {
                    $msWMIAuthor = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
                }
            }
            catch [Exception] 
            {
                $msWMIAuthor = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
            }
        }

        if ([System.String]::IsNullOrEmpty($Description)) 
        {
            $Description = $Name
        }

        $msWMIParm2 = $Expression.Count.ToString() + ";"

        foreach($Exp in $Expression)
        {
            $msWMIParm2 += "3;10;$($Exp.Length);WQL;$Namespace;$Exp;"
        }

        $msWMICreationDate = (Get-Date).ToUniversalTime().ToString("yyyyMMddhhmmss.ffffff-000")
        $msWMIChangeDate = $msWMICreationDate

        $Replaced = $false

        if ($WithReplace) 
        {
            $Filter = Get-ADObject -Filter {(objectClass -eq "msWMI-SOM") -and (msWMI-Name -eq $Name)} -Server $Server @CredSplat
            
            if ($Filter -ne $null) 
            {
                $Filter = $Filter | Select-Object -First 1

                $ADObject = Set-ADObject -Identity $Filter.DistinguishedName -Replace @{"msWMI-Parm2"=$msWMIParm2;"msWMI-ChangeDate"=$msWMIChangeDate} -Server $Server -PassThru @CredSplat
                $Replaced = $true
            }
        }

        if ($Replaced -eq $false) 
        {            
            $WMIGUID = "{" + ([System.Guid]::NewGuid().ToString()) + "}"
            $msWMIParm1 = $Description
       
            $Attr = @{
                "msWMI-Name" = $Name;
                "msWMI-Parm1" = $msWMIParm1;
                "msWMI-Parm2" = $msWMIParm2;
                "msWMI-Author" = $msWMIAuthor;
                "msWMI-ID"= $WMIGUID;
                "instanceType" = 4;
                "showInAdvancedViewOnly" = "TRUE";
                "msWMI-ChangeDate" = $msWMICreationDate; 
                "msWMI-CreationDate" = $msWMICreationDate
            }

            $WMIPath = "CN=SOM,CN=WMIPolicy,CN=System,$DefaultNamingContext"

            $ADObject = New-ADObject -Name $WMIGUID -Type "msWMI-Som" -Server $Server -Path $WMIPath -OtherAttributes $Attr -PassThru @CredSplat
        }

        if ($PassThru)
        {
            Write-Output -InputObject $ADObject
        }
    }

    End {
    }
}

Function Set-PDCEmulatorSrvRecords {
    <#
        .SYNOPSIS
            Changes the PDC Emulator Srv DNS Records for every domain in the forest.
 
        .DESCRIPTION
            The cmdlet defaults to reducing the priority and increasing the weight of the PDC Emulator in every domain so that it does not process logons.
 
        .PARAMETER Priority
            The priority to set on the DNS Srv Records for each PDC.
 
        .PARAMETER Weight
            The weight to set on the DNS Srv Records for each PDC.
 
        .PARAMETER RestartServer
            Indicate whether to restart the server in order to complete the change.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
            The user must have ENTERPRISE ADMIN privileges to connect to each PDC Emulator in each domain.
 
        .PARAMETER PassThru
            Returns an object that represents the updated dns records for each PDC Emulator.
 
        .EXAMPLE
            Set-PDCEmulatorSrvRecords
             
            Sets the priority to 200 and weight to 25.
 
        .EXAMPLE
            Set-PDCEmulatorSrvRecords -Priority 150 -Weight 20
             
            Sets the priority to 150 and weight to 20.
 
        .EXAMPLE
            Set-PDCEmulatorSrvRecords -RestartServer
             
            Sets the priority to 200 and weight to 25 and restarts the server immediately to implement the change.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    Param
    (
        [Parameter(Position = 0)]
        [System.Int32]$Priority = 200,

        [Parameter(Position = 1)]
        [System.Int32]$Weight = 25,

        [Parameter(Position = 2)]
        [System.Boolean]$RestartServer = $false,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Forest = Get-ADForest -Current LoggedOnUser @CredSplat
        $Domains = $Forest.Domains

        foreach ($Domain in $Domains)
        {
            Write-Verbose -Message "Setting SRV records in domain $Domain"

            $PDCEmulator = Get-ADDomain -Identity $Domain @CredSplat | Select-Object -ExpandProperty PDCEmulator

            Write-Verbose -Message "Identified PDC Emulator as $PDCEmulator"

            $Result = Invoke-Command -ComputerName $PDCEmulator -ScriptBlock {
                Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters -Name "LdapSrvPriority" -Value $args[0]
                Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters -Name "LdapSrvWeight" -Value $args[1]

                $SrvPriority = Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters -Name "LdapSrvPriority" | Select-Object -ExpandProperty LdapSrvPriority
                $SrvWeight = Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters -Name "LdapSrvWeight" | Select-Object -ExpandProperty LdapSrvWeight

                Write-Output -InputObject (New-Object -TypeName PSCustomObject -Property @{"LdapSrvPriority" = $SrvPriority; "LdapSrvWeight" = $SrvWeight})
            } -ArgumentList @($Priority,$Weight) @CredSplat

            if ($RestartServer)
            {
                Write-Host -Object "Restarting $PDCEmulator now."
                Restart-Computer -ComputerName $PDCEmulator -Wait -Credential @CredSplat
            }
            else
            {
                Write-Warning -Message "The change will not take effect until $PDCEmulator is restarted."
            }

            if ($PassThru) 
            {
                Write-Output -InputObject (New-Object -TypeName PSCustomObject -Property @{"Domain" = $Domain; "PDC Emulator" = $PDCEmulator; "LdapSrvPriority" = $Result.LdapSrvPriority; "LdapSrvWeight" = $Result.LdapSrvWeight})
            }     
        }
    }

    End {   
    }
}

Function Start-SDProp {
    <#
        .SYNOPSIS
            Runs the SDProp process immediately in the current domain.
 
        .DESCRIPTION
            The function sets the runProtectAdminGroupsTask on the RootDSE object to 1 and runs SDProp.
 
        .EXAMPLE
            Start-SDProp
 
            Launches the SDProp process.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/5/2015
    #>

    
    [CmdletBinding()]
    Param()

    Begin {}

    Process{
        $RootDSE = [ADSI]"LDAP://RootDSE"
        $RootDSE.Put("runProtectAdminGroupsTask", 1)
        $RootDSE.SetInfo()
    }

    End {}
}

Function Set-SDPropSchedule {
    <#
        .SYNOPSIS
            Sets the schedule that the SDProp process runs.
 
        .DESCRIPTION
            The cmdlet sets the AdminSDProtectFrequency registry key value with the desired schedule in seconds. The cmdlet must be run on the PDC Emulator for it to be effective.
 
            The cmdlet requires confirmation because of the impact of the change.
 
        .PARAMETER Seconds
            The number of seconds in between SDProp execution tasks. This can be from 60 seconds (1 minute) to 7200 seconds (2 hours). Setting this value less than
            the default of 3600 seconds is not recommended for performance reasons.
 
        .PARAMETER FindPDCEmulator
            If this cmdlet is not being run on the PDC emulator for the domain, you can specify this parameter to find the PDC emulator in the domain and execute the change
            on that server.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
            The credential is used to connect to a remote computer if the FindPDCEmulator parameter is specified. If the cmdlet is being run locally, this parameter is ignored.
 
        .PARAMETER Domain
            The domain to execute the schedule update on. If this is not specified, it defaults to the domain of the current user.
 
        .EXAMPLE
            Set-SDPropSchedule -Seconds 5400
 
            Sets the schedule for the SDProp task to run every 5400 seconds (90 minutes).
 
        .EXAMPLE
            Set-SDPropSchedule -Seconds 5400 -FindPDCEmulator -Credential (Get-Credential)
 
            Sets the schedule for the SDProp task to run every 5400 seconds (90 minutes) on the PDC emulator discovered for the current user's domain.
 
        .INPUTS
            System.Int32
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
            Returns a custom object that contains the updated registry property.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/8/2017
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateRange(60, 7200)]
        [System.Int32]$Seconds,

        [Parameter(ParameterSetName = "Find")]
        [Switch]$FindPDCEmulator,

        [Parameter(ParameterSetName = "Find")]
        [System.String]$Domain,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
        Function Set-Value {
            [CmdletBinding()]
            Param(
                [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
                [ValidateRange(60, 7200)]
                [System.Int32]$Seconds,

                [Parameter(Position = 1)]
                [ValidateNotNull()]
                [System.Management.Automation.Credential()]
                [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

                [Parameter(Position = 2)]
                [Microsoft.Management.Infrastructure.CimSession]$CimSession
            )

            Begin {
            }

            Process {
                $CredSplat = @{}

                if ($Credential -ne [PSCredential]::Empty)
                {
                    $CredSplat["Credential"] = $Credential
                }

                if ($CimSession -eq $null)
                {
                    $CimSession = New-CimSession
                }

                $Path = "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters"
                $Key = "AdminSDProtectFrequency"
                
                $Role = Get-CimInstance -ClassName Win32_ComputerSystem -Property DomainRole -Namespace "root\cimv2" -CimSession $CimSession -ErrorAction Stop | Select-Object -ExpandProperty DomainRole

                if ($Role -ne 5)
                {
                    Write-Warning -Message "This computer is not the PDC emulator, this cmdlet will not run."
                }
                else
                {
                    if (-not (Test-Path -Path $Path))
                    {
                        Write-Verbose -Message "Creating registry key $Path"
                        New-Item -Path $Path -ErrorAction Stop @CredSplat | Out-Null
                    }

                    New-ItemProperty -Path $Path -Name $Key -Value $Seconds -PropertyType DWORD -Force @CredSplat
                }
            }
             
            End {
            }
        }
    }

    Process {        
        $Message = "Set frequency to $Seconds"
        $Path = "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters"
        $Key = "AdminSDProtectFrequency"

        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ($Seconds -le 3600)
        {
            Write-Warning -Message "Setting the frequency to less than 3600 seconds (1 hour) could have potential LSASS performance ramifications in a large environment, i.e. doing this could cause your DC�s processor to spike to very high sustained levels and drastically hurt you."
            $Message = "Set frequency to $Seconds, which is less than the recommended value of 3600"
        }
        
        if ($PSCmdlet.ShouldProcess("$Path - $Key", $Message))
        {
            if ($FindPDCEmulator)
            {
                if ([System.String]::IsNullOrEmpty($Domain))
                {
                    $Domain = $env:USERDOMAIN
                }

                $PDC = Get-ADDomain -Identity $Domain @CredSplat | Select-Object -ExpandProperty PDCEmulator
                
                Invoke-Command -ComputerName $PDC -ScriptBlock ${function:Set-Value} -ArgumentList @($Seconds) $CredSplat
            }
            else
            {
                Set-Value -Seconds $Seconds @CredSplat
            }
        }
    }

    End {
    }
}

Function Grant-SPNWriteOnProtectedADObjects {
    <#
        .SYNOPSIS
            The NETWORK SERVICE principal attempts to register SPNs on servers, most commonly WSMAN, but is unable to due to default permissions.
            Explicity setting an Access Control Entry (ACE) on the computer object will only temporarily work in some cases because the AdminSDHolder ACL does not include
            the NETWORK SERVICE principal and the recurring SDProp task replaces protected objects' ACLs with the ACL on the AdminSDHolder object. Domain controllers are the
            typical example of computer objects affected by this security design. This cmdlet sets the permissions on the AdminSDHolder object so that updated ACL entries
            on protected objects, like domain controllers, are not overwritten.
             
        .DESCRIPTION
            This cmdlet runs against each domain in the current forest and Enterprise Admin privileges are required.
 
            The Grant-SPNWriteOnProtectedADObjects cmdlet adds the NETWORK SERVICE principal with the "Validated Write to Service Principal Name" right to the AdminSDHolder
            container so that protected objects do not have their ACLs overwritten when they are fixed to resolve EventId 10154
 
            The error that is received on a protected computer/server will look like the following if this command needs to be run:
 
            -----------
 
            The WinRM service failed to create the following SPNs: WSMAN/servername.fqdn; WSMAN/servername
             
            Additional Data
            The error received was 1355: %%1355.
            User Action
            The SPN can be created by an administrator using setspn.exe utility.
 
            -----------
 
            A background process, Security Descriptor Propagator Update (SDProp), on the PDC emulator runs every 60 minutes (by default) and compares the ACL of the AdminSDHolder
            object with the ACL of the protected users, group and computers. If there are any differences it overwrites them.
 
        .PARAMETER IncludeDomainControllers
            The updated ACL entries are also added to the domain controllers in the defined domain.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Grant-SPNWriteOnProtectedADObjects
 
            Configures the permissions on the AdminSDHolder object in each domain of the current forest.
 
        .EXAMPLE
            Grant-SPNWriteOnProtectedADObjects -IncludeDomainControllers
 
            Configures the permissions on the AdminSDHolder object in each domain of the current forest. It also enables the NETWORK SERVICE principal to write SPNs
            on domain controller objects.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding()]
    Param(
        [Parameter()]
        [Switch]$IncludeDomainControllers,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin{
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if (Test-IsEnterpriseAdmin @CredSplat)
        {
            [Microsoft.ActiveDirectory.Management.ADForest]$Forest = Get-ADForest -Current LoggedOnUser @CredSplat
            [System.String]$ForestDN = (Get-ADDomain -Identity ($Forest.RootDomain) @CredSplat).DistinguishedName
            [System.String[]]$Domains = $Forest.Domains

            foreach ($Domain in $Domains)
            {
                [System.Security.Principal.SecurityIdentifier]$DomainSID = Get-ADDomain -Identity $Domain @CredSplat | Select-Object -ExpandProperty DomainSID

                #f3a64788-5306-11d1-a9c5-0000f80367c1
                $ExtendedRightGuid = Get-ADObject -Identity ("CN=Validated-SPN,CN=Extended-Rights,CN=Configuration," + $ForestDN) -Properties RightsGUID @CredSplat | Select-Object -ExpandProperty RightsGUID
                [System.Security.Principal.SecurityIdentifier]$NetworkService = New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::NetworkServiceSid, $DomainSID)

                $Ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
                    $NetworkService, 
                    [System.DirectoryServices.ActiveDirectoryRights]::Self, 
                    [System.Security.AccessControl.AccessControlType]::Allow, 
                    $ExtendedRightGuid, 
                    [System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
                )

                [System.DirectoryServices.ActiveDirectoryAccessRule[]] $Rules = @($Ace)

                Set-ADObjectAcl -Rules $Rules -ObjectDN "CN=AdminSDHolder,CN=System,$($Domain.Replace(".","DC="))" @CredSplat
                
                if ($IncludeDomainControllers)
                {
                    $Context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext(
                        [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain,
                        $Domain)

                    [System.DirectoryServices.ActiveDirectory.DomainControllerCollection]$DomainControllers = [System.DirectoryServices.ActiveDirectory.DomainController]::FindAll($Context)

                    foreach ($DomainController in $DomainControllers)
                    {
                        $Path = Get-ADDomainController -Identity $DomainController.Name -Server $DomainController.Name | Select-Object -ExpandProperty ComputerObjectDN

                        Set-ADObjectAcl -Rules $Rules -ObjectDN $Path 
                    }
                }
            }
        }
        else
        {
            Write-Error -Message "Current user is not an Enterprise Admin, run the command again with Enterprise Admin credentials."
            throw [System.UnauthorizedAccessException]("Current user is not an Enterprise Admin, run the command again with Enterprise Admin credentials.")
        }
    }
    
    End {
    }
}

Function Set-ADObjectAcl {
    <#
        .SYNOPSIS
            Sets permissions on an Active Directory object.
 
        .DESCRIPTION
            Will set permissions on an Active Directory object with the provided rule set and Object CN. ACLs for security principals that are defined as part of
            the rule set provided will be replaced with the updated rules. Other ACLs rules will not be affected unless the Replace switch is defined.
 
        .EXAMPLE
            Set-ADObjectAcl -Domain contoso.com -ObjectCN "CN=AdminSDHolder,CN=System" -Rules $Rules
 
            Adds permissions to the AdminSDHolder container in the contoso.com domain with the ACL rules provided.
 
        .EXAMPLE
            Set-ADObjectAcl -ObjectDN "CN=AdminSDHolder,CN=System,DC=contso,DC=com" -Rules $Rules -Replace
 
            Sets permissions on the AdminSDHolder container in the contoso.com domain with the ACL rules provided. All existing ACL entries are replaced
            with the provided rules.
 
        .PARAMETER Domain
            The domain in which to configure the object's ACL.
 
        .PARAMETER ObjectCN
            The CN of the object being configured up to the domain part of the DN. This can be an empty string to configure the domain object.
 
        .PARAMETER ObjectDN
            The DistinguishedName of the object being configured.
 
        .PARAMETER Rules
            An array of Active Directory Access Rules
 
        .PARAMETER ReplaceForExistingPrincipal
            Indicates that existing ACL entries that have a common security principal as a new defined rule should be replaced with the newer rule. If the Replace
            parameter is specified, this parameter is ignored.
 
        .PARAMETER Replace
            Indicates that all ACL entries on the object should be replaced with the provided rules. If this is not specified, the provided rules are only
            added to the existing rules.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .INPUTS
            System.String
 
            System.DirectoryServices.ActiveDirectoryAccessRule[]
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>

    
    [CmdletBinding(DefaultParameterSetName = "DN")]
    Param 
    (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "CN")]
        [AllowEmptyString()]
        [System.String]$ObjectCN,

        [Parameter(Position = 1, ParameterSetName = "CN")]
        [System.String]$Domain,

        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "DN", ValueFromPipeline = $true)]
        [System.String]$ObjectDN,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.DirectoryServices.ActiveDirectoryAccessRule[]]$Rules,

        [Parameter()]
        [Switch]$Replace,

        [Parameter()]
        [Switch]$ReplaceForExistingPrincipal,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {   
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ([System.String]::IsNullOrEmpty($Domain))
        {
            $Domain = $env:USERDOMAIN
        }
        
        [Microsoft.ActiveDirectory.Management.ADDomain]$Temp = Get-ADDomain -Current LoggedOnUser -ErrorAction Stop @CredSplat
        $DomainDN = $Temp.DistinguishedName
        $Domain = $Temp.DNSRoot

        $DC = Get-ADDomainController -DomainName $Domain -Discover
        [System.String[]]$Drives = Get-PSDrive | Select-Object -ExpandProperty Name

        if ($DC -ne $null)
        {
            if (Test-Connection -ComputerName "$($DC.Name).$($DC.Domain)" -Count 1)
            {
                $TempDrive = "tempdrive"

                if ($Drives.Contains($TempDrive))
                {
                    Write-Host "An existing PSDrive exists with name $TempDrive, temporarily removing" -ForegroundColor Yellow
                    $OldDrive = Get-PSDrive -Name $TempDrive
                    Remove-PSDrive -Name $TempDrive
                }

                $Drive = New-PSDrive -Name $TempDrive -Root "" -PSProvider ActiveDirectory -Server "$($DC.Name).$($DC.Domain)" @CredSplat

                Push-Location -Path "$Drive`:\"

                switch ($PSCmdlet.ParameterSetName)
                {
                    "CN" {
                        if ([System.String]::IsNullOrEmpty($ObjectCN))
                        {
                            $ObjectDN = $DomainDN
                        }
                        else
                        {
                            $ObjectDN = $ObjectCN + "," + $DN
                        }
                        break
                    }
                    "DN" {
                        #Do nothing
                        break
                    }
                    default {
                        throw "Could not determine parameter set name."
                    }
                }

                [System.String[]]$Paths = @()

                #Make sure the object exists
                $ObjectToChange = Get-ADObject -Identity $ObjectDN -Server "$($DC.Name).$($DC.Domain)" -ErrorAction Stop @CredSplat
                $Path = $Drive.Name + ":" + $ObjectDN

                Write-Verbose -Message "Modifying ACL on $Path."

                try
                {
                    $Acl = Get-Acl -Path $Path

                    if ($Acl -ne $null)
                    {
                        if ($Replace)
                        {
                            $OldAcls = $Acl.Access
                        }
                        elseif ($ReplaceForExistingPrincipal)
                        {
                            $OldAcls = $Acl.Access | Where-Object {$Rules.IdentityReference -eq  $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier])}
                        }
                        else
                        {
                            $OldAcls = @()
                        }

                        foreach ($Rule in $OldAcls)
                        {
                            $Acl.RemoveAccessRule($Rule) | Out-Null
                        }

                        foreach ($Rule in $Rules)
                        {
                            $Acl.AddAccessRule($Rule) | Out-Null
                        }

                        $NewAcl = Set-Acl -Path $Path -AclObject $Acl -Passthru
                    
                        Write-Host -Object $Domain -ForegroundColor DarkRed -BackgroundColor White
                        Write-Host -Object ($Path.Substring($Path.IndexOf(":") + 1)) -ForegroundColor DarkRed -BackgroundColor White
                        Write-Host -Object ($NewAcl.Access | Format-List | Out-String)
                        Write-Host -Object ""
                    }
                    else
                    {
                        Write-Warning "Could not retrieve the ACL for $Path"
                    }
                }
                catch [System.Exception]
                {
                    Write-Warning -Message $_.Exception.Message
                }

                Pop-Location

                Remove-PSDrive $Drive

                if ($OldDrive)
                {
                    Write-Host "Recreating original PSDrive" -ForegroundColor Yellow
                    New-PSDrive -Name $OldDrive.Name -PSProvider $OldDrive.Provider -Root $OldDrive.Root | Out-Null
                    Remove-Variable -Name "OldDrive"
                }
            }
            else
            {
                Write-Warning -Message "Could not contact domain controller $($DC.Name).$($DC.Domain)."
            }
        }
        else
        {
            Write-Warning -Message "Could not locate a domain controller in $Domain."
        }
    }

    End {}
}

Function Get-KerberosDelegationInformation {
    <#
        .SYNOPSIS
            Gets Kerberos delegation information for a delegated server and its target.
 
        .DESCRIPTION
            The cmdlet retrieves the delegation information and UAC information for delegation permissions. It also retrieves the registered SPNs for the target servers.
 
        .PARAMETER DelegatedServer
            The server that is granted Kerberos delegation permissions.
 
        .PARAMETER TargetServer
            The server that is the target of the delegation.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Get-KerberosDelegationInformation -DelegatedServer WebServer1 -TargetServer AppServer1
 
            Gets the delegation information for WebServer1 to AppServer1.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>


    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$DelegatedServer,

        [Parameter(Position = 1, Mandatory = $true)]
        [System.String]$TargetServer,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Delegation = Get-ADComputer -Identity $DelegatedServer -Properties msDS-AllowedToDelegateTo,UserAccountControl @CredSplat | Select-Object -Property msDS-AllowedToDelegateTo,UserAccountControl
        [System.String[]]$SPNs = (Get-ADComputer -Identity $TargetServer -Properties ServicePrincipalName @CredSplat | Select-Object -ExpandProperty ServicePrincipalName).Split("`n")
        
        $Matches = @()
        foreach ($Value in $script:UACValues) 
        {
            #Perform bitwise and to compare the current UAC against each possible value and record matches
            if ($Delegation.UserAccountControl -band [System.Convert]::ToInt64($Value.Key, 16)) 
            {
                $Matches += $Value
            }
        }

        Write-Output -InputObject ([PSCustomObject]@{DelegatedServer=$DelegatedServer;AllowedToDelegateTo=$Delegation.'msDS-AllowedToDelegateTo';UAC=$Matches;TargetServer=$TargetServer;TargetSPNs=$SPNs})
    }

    End {
    }
}

Function Test-IsEnterpriseAdmin {
    <#
        .SYNOPSIS
            Tests if a user is a member of the Enterprise Admins group.
 
        .DESCRIPTION
            The Test-IsEnterpriseAdmin returns true if the user is in the group and false otherwise.
 
        .EXAMPLE
            Test-IsEnterpriseAdmin
 
            Determines if the user credentials being used to run the cmdlet have Enterprise Admin privileges.
         
        .EXAMPLE
            Test-IsEnterpriseAdmin -UserName "John Smith"
 
            Determines if the user John Smith has Enterprise Admin privileges.
 
        .EXAMPLE
            Test-IsEnterpriseAdmin -Credential (Get-Credential)
             
            Determines if the entered user credentials have Enterprise Admin privileges.
 
        .PARAMETER UserName
            The user to test the group membership on. If no user name is specified, the cmdlet runs against the current WindowsIdentity Principal.
 
        .PARAMETER Credential
            The PSCredential to use to test if the user has Enterprise Admin credentials. These credentials are not used to execute commands.
 
        .INPUTS
            System.Management.Automation.PSCredential
 
            System.String
 
        .OUTPUTS
            System.Boolean
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding(DefaultParameterSetName="Username")]
    Param (
        [Parameter(Position = 0, ValueFromPipeLine = $true, ParameterSetName = "Username")]
        [System.String]$UserName = [System.String]::Empty,

        [Parameter(Position = 0, ValueFromPipeLine = $true, ParameterSetName = "Credential")]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process 
    {
        if ($PSCmdlet.ParameterSetName -eq "Credential")
        {
            $UserName = $Credential.UserName
        }

        $Details = Get-ADPrincipalDetails -Identity $UserName

        Write-Output -InputObject $Details.EnterpriseAdmin
    }

    End {        
    }
}

Function Test-IsEnterpriseOrDomainAdmin {
    <#
        .SYNOPSIS
            Tests whether the current user or provided credential is a Domain Admin or Enterprise Admin.
 
        .DESCRIPTION
            The Test-IsEnterpriseOrDomainAdmin returns true if the user is in the group and false otherwise.
 
        .EXAMPLE
            Test-IsEnterpriseOrDomainAdmin
             
            Determines if the user credentials being used to run the cmdlet have Domain or Enterprise Admin privileges
 
        .EXAMPLE
            Test-IsEnterpriseOrDomainAdmin -Credential $Creds
 
            Determines if the credentials have Enterprise or Domain Admin privileges
 
        .PARAMETER UserName
            The user to test the group membership on. If no user name is specified, the cmdlet runs against WindowsIdentity Principal.
 
        .PARAMETER Credential
            The PSCredential to use to test if the user has Enterprise Admin credentials. These credentials are not used to execute commands.
 
        .PARAMETER Domain
            The domain to check for privileges in. This defaults to the current user's domain.
 
        .INPUTS
            System.Management.Automation.PSCredential
 
            System.String
 
        .OUTPUTS
            System.Boolean
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding(DefaultParameterSetName="Username")]
    Param
    (
        [Parameter(Position = 0, ValueFromPipeLine = $true, ParameterSetName = "Credential")]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Position = 0, ValueFromPipeLine = $true, ParameterSetName = "Username")]
        [System.String]$Username = [System.String]::Empty,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty
    )

    Begin {
    }

    Process
    {        
        if ($PSCmdlet.ParameterSetName -eq "Credential")
        {
            $UserName = $Credential.UserName
        }

        $Details = Get-ADPrincipalDetails -Identity $UserName -Domain $Domain

        Write-Output -InputObject ($Details.DomainAdmin -or $Details.EnterpriseAdmin)
    }
    
    End {        
    }
}

Function Test-IsDomainAdmin {
    <#
        .SYNOPSIS
            Tests if a user is a member of the Domain Admins group.
 
        .DESCRIPTION
            The Test-IsDomainAdmin returns true if the user is in the Domain Admins group and false otherwise.
 
        .EXAMPLE
            Test-IsDomainAdmin
 
            Determines if the user credentials being used to run the cmdlet has Domain Admin privileges.
 
        .EXAMPLE
            Test-IsDomainAdmin -UserName "John Smith"
 
            Determines if the user John Smith has Domain Admin privileges
 
        .PARAMETER UserName
            The user to test the group membership on. If no user name is specified, the cmdlet runs against the current WindowsIdentity Principal.
 
        .PARAMETER Credential
            The PSCredential to use to test if the user has Domain Admin credentials. These credentials are not used to execute commands.
 
        .PARAMETER Domain
            The domain to check for privileges in. This defaults to the logged on user's domain.
 
        .INPUTS
            System.Management.Automation.PSCredential
             
            System.String
 
        .OUTPUTS
            System.Boolean
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding(DefaultParameterSetName="Username")]
    Param (
        [Parameter(Position = 0, ValueFromPipeLine = $true, ParameterSetName = "Username")]
        [System.String]$UserName = [System.String]::Empty,

        [Parameter(Position = 0, ValueFromPipeLine = $true, ParameterSetName = "Credential")]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty,

        [Parameter(Position = 1)]
        [System.String]$Domain
    )

    Begin {
    }

    Process 
    {
        if ($PSCmdlet.ParameterSetName -eq "Credential")
        {
            $UserName = $Credential.UserName
        }

        $Details = Get-ADPrincipalDetails -Identity $UserName -Domain $Domain

        Write-Output -InputObject $Details.DomainAdmin
    }

    End {        
    }
}

Function ConvertTo-ADObject {
    <#
        .SYNOPSIS
            Takes an identity and an optional domain and returns information about a matching AD object.
 
        .DESCRIPTION
            The cmdlet takes the identity and matches it against properties to include:
             
                -Name
                -SamAccountName
                -CN
                -DistinguishedName
                -Display Name
                -ObjectSID
                -ObjectGUID
 
            Only user, computer, group, and foreignSecurityPrincipal object types are searched and returned. If there are multiple matches to the identity
            value, only the first match is returned. Additionally, information about the object's domain and forest are also included in the return object.
 
        .EXAMPLE
            ConvertTo-ADObject -Identity "Domain Admins" -Domain contoso
 
            Gets information about the AD object in contoso that matches "Domain Admins"
 
        .PARAMETER Identity
            The identity to convert to an AD object. This is a string that is matched against AD object properties:
 
                -Name
                -SamAccountName
                -CN
                -DistinguishedName
                -Display Name
                -ObjectSID
                -ObjectGUID
 
            For user objects, the identity can be provided as a User Principal Name (UPN) or domain\username format. In these cases, the domain will be extracted
            from the identity and does not need to be specified.
 
            If this parameter is not specified, the current windows identity principal is utilized, which could be a user or system account, which is translated to the corresponding
            computer object.
 
        .PARAMETER Domain
            Optionally indicates the domain the object exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the Identity parameter. If the identity parameter is specified, the current user domain is used. If the identity parameter is not specified, and the current windows identity is
            the system account, the local computer domain is used, otherwise the current user domain is used.
 
            If the Identity parameter is specified with a domain\username or UPN value, this parameter is ignored.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeLine = $true, Mandatory= $true)]
        [AllowEmptyString()]
        [System.String]$Identity = [System.String]::Empty,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ([System.String]::IsNullOrEmpty($Identity))
        {
            #If the identity wasn't specified, then we'll use the current identity, there are two options, either it is the system or service
            #account, or it is a user

            [System.Security.Principal.WindowsIdentity]$Principal = [System.Security.Principal.WindowsIdentity]::GetCurrent()
            
            #NT AUTHORITY\SYSTEM is the name attribute for the system account
            if ($Principal.IsSystem) 
            {
                $Identity = $env:COMPUTERNAME
                
                Write-Verbose -Message "Current principal is the SYSTEM account for $Identity."
            }
            else
            {
                $Identity = $Principal.Name
            }
        }

        if ($Identity.IndexOf("\") -ne -1) 
        {
            $TempDomain = $Identity.Substring(0, $Identity.IndexOf("\"))
            [Microsoft.ActiveDirectory.Management.ADDomain]$ADDomain = Get-ADDomain -Identity $TempDomain @CredSplat
            $Identity = $Identity.Substring($Identity.IndexOf("\") + 1)
        }
        #Identity is in a UPN format
        elseif ($Identity.IndexOf("@") -ne -1) 
        {
            $Domain = $Identity.Substring($Identity.IndexOf("@") + 1)
            [Microsoft.ActiveDirectory.Management.ADDomain]$ADDomain = Get-ADDomain -Identity $Domain @CredSplat
            $Identity = $Identity.Substring(0, $Identity.IndexOf("@"))
        }
        #Identity has no domain defined
        else 
        {
            if ([System.String]::IsNullOrEmpty($Domain))
            {
                [Microsoft.ActiveDirectory.Management.ADDomain]$ADDomain = Get-ADDomain -Current LoggedOnUser @CredSplat
            }
            else
            {
                [Microsoft.ActiveDirectory.Management.ADDomain]$ADDomain = Get-ADDomain -Identity $Domain @CredSplat
            }
        }

        Write-Verbose -Message "Finding details for $Identity in $($ADDomain.DNSRoot)."

        [Microsoft.ActiveDirectory.Management.ADObject]$ADObj = Get-ADObject -Filter { Name -eq $Identity -or SamAccountName -eq $Identity -or DisplayName -eq $Identity -or CN -eq $Identity -or DistinguishedName -eq $Identity -or ObjectSID -eq $Identity -or ObjectGUID -eq $Identity } `
            -Server $ADDomain.DNSRoot -Properties memberOf,msds-principalName,userAccountControl @CredSplat | Where-Object {$_.ObjectClass -in @("user", "computer", "group", "foreignSecurityPrincipal", "contact")} | Select-Object -First 1

        Write-Output -InputObject ([PSCustomObject]@{Identity = $ADObj; Domain = $ADDomain; Forest = (Get-ADDomain -Identity $ADDomain.Forest -Server $ADDomain.Forest @CredSplat) })
    }

    End {
    }
}

Function Get-ADGroupMembershipChangeSummary {
    <#
        .SYNOPSIS
            Finds all of the group membership changes in the logs on all of the domain controllers in a domain.
 
        .DESCRIPTION
            Finds all of the group membership changes in the logs on all of the domain controllers in a domain. It returns these logs in several formats that can be selected. The default output is a custom object.
 
        .PARAMETER DaysAgo
            Specifies how many days in the past the search should start.
 
        .PARAMETER Domain
            The domain to perform the search on. This defaults to the domain of the current user.
 
        .PARAMETER AsJson
            Returns the report as a Json string.
 
        .PARAMETER AsXml
            Returns the report as an Xml string.
 
        .PARAMETER AsHtml
            Returns the report as a custom formatted html document.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Get-ADGroupMembershipChangeSummary -DaysAgo 1 -Domain "contoso.com"
 
            Gets all of the group membership changes in the past day from contoso.com
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
                This is returned when AsJson, AsHtml, or AsXml are specified.
 
            System.Management.Automation.PSObject
 
                This is returned when no transform of the result is specified.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/4/2017
    #>


    [CmdletBinding(DefaultParameterSetName = "default")]
    Param(
        [Parameter(Position = 0)]
        [System.Int32]$DaysAgo = 1,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter(ParameterSetName="Json")]
        [Switch]$AsJson,

        [Parameter(ParameterSetName="Xml")]
        [Switch]$AsXml,

        [Parameter(ParameterSetName="Html")]
        [Switch]$AsHtml,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        if ($Domain -eq [System.String]::Empty)
        {
            $Domain = (Get-ADDomain -Current LoggedOnUser).DnsRoot
        }

        $Context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain, $Domain)
        $Report = New-Object -TypeName PSObject -Property @{"Added To Domain Local Group"=@();"Removed From Domain Local Group"=@();"Added To Global Group"=@();"Removed From Global Group"=@();"Added To Universal Group"=@();"Removed From Universal Group"=@()}

        <#
            Event ID 636 & 4732 - Member added to domain local group
            Event ID 637 & 4733 - Member removed from domain local group
            Event ID 632 & 4728 - Member added to global group
            Event ID 633 & 4729 - Member removed from domain local group
            Event ID 660 & 4756 - Member added to universal group
            Event ID 661 & 4757 - Member removed from universal group
        #>

        $Count = 0
        [System.DirectoryServices.ActiveDirectory.DomainController]::FindAll($Context) | ForEach-Object {
            Write-Verbose -Message "Evaluating server $($_.Name)."
            $Count++

            [System.Version]$Version = [System.Environment]::OSVersion.Version
            [System.Decimal]$Version = [System.Decimal]::Parse("$($Version.Major).$($Version.Minor)")

            if ($Version -ge 6.1)
            {
                [System.Diagnostics.Eventing.Reader.EventLogRecord[]]$Events = Get-WinEvent -ComputerName $_.Name -FilterHashtable @{LogName=@("Security");ID=@(636,4732,637,4733,632,4728,633,4729,660,4756,661,4757);StartTime=(Get-Date).AddDays($DaysAgo * -1)} -ErrorAction SilentlyContinue @CredSplat

                if ($Events -ne $null -and $Events.Count -ge 1)
                {
                    #Domain Local Groups
                    $Report."Added To Domain Local Group" += $Events | Where-Object {$_.ID -eq 636 -or $_.ID -eq 4732} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeCreated.DateTime} },Message
                    $Report."Removed From Domain Local Group" += $Events | Where-Object {$_.ID -eq 637 -or $_.ID -eq 4733} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeCreated.DateTime} },Message

                    #Global Groups
                    $Report."Added To Global Group" += $Events | Where-Object {$_.ID -eq 632 -or $_.ID -eq 4728} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeCreated.DateTime} },Message
                    $Report."Removed From Global Group" += $Events | Where-Object {$_.ID -eq 633 -or $_.ID -eq 4729} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeCreated.DateTime} },Message

                    #Universal Groups
                    $Report."Added To Universal Group" += $Events | Where-Object {$_.ID -eq 660 -or $_.ID -eq 4756} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeCreated.DateTime} },Message
                    $Report."Removed From Universal Group" += $Events | Where-Object {$_.ID -eq 661 -or $_.ID -eq 4757} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeCreated.DateTime} },Message
                }
                else
                {
                    Write-Verbose -Message "No events found on $($_.Name)."
                }
            }
            else
            {
                [System.Diagnostics.EventLogEntry[]]$Events = Get-EventLog -LogName "Security" -ComputerName $_.Name -After ((Get-Date).AddDays($DaysAgo * -1)) -ErrorAction SilentlyContinue | Where-Object {$_.EventID -in @(636,4732,637,4733,632,4728,633,4729,660,4756,661,4757) } | Sort-Object -Descending -Property TimeGenerated

                if ($Events -ne $null -and $Events.Count -ge 1)
                {
                    #Domain Local Groups
                    $Report."Added To Domain Local Group" += $Events | Where-Object {$_.ID -eq 636 -or $_.ID -eq 4732} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeGenerated} },Message
                    $Report."Removed From Domain Local Group" += $Events | Where-Object {$_.ID -eq 637 -or $_.ID -eq 4733} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeGenerated} },Message

                    #Global Groups
                    $Report."Added To Global Group" += $Events | Where-Object {$_.ID -eq 632 -or $_.ID -eq 4728} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeGenerated} },Message
                    $Report."Removed From Global Group" += $Events | Where-Object {$_.ID -eq 633 -or $_.ID -eq 4729} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeGenerated} },Message

                    #Universal Groups
                    $Report."Added To Universal Group" += $Events | Where-Object {$_.ID -eq 660 -or $_.ID -eq 4756} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeGenerated} },Message
                    $Report."Removed From Universal Group" += $Events | Where-Object {$_.ID -eq 661 -or $_.ID -eq 4757} | Select-Object -Property MachineName,@{Name="TimeCreated"; Expression ={$_.TimeGenerated} },Message
                }
                else
                {
                    Write-Verbose -Message "No events found on $($_.Name)."
                }
            }
        }

        switch ($PSCmdlet.ParameterSetName) {
            "Json" {
                Write-Output -InputObject (ConvertTo-Json -InputObject $Report)
                break
            }
            "Xml" {
                Write-Output -InputObject (ConvertTo-Xml -InputObject $Report -Depth 2 -As String)
                break
            }
            "Html" {
                $Date = (Get-Date).ToString()

                $HtmlString = @"
<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Group Membership Change Report</title>
    </head>
    <style>
        .logtable {
            width:100%;
            table-layout:fixed;
            border:1px solid black;
        }
 
        .timecolumn {
            width:25%;
        }
 
        .messagecolumn {
            width:75%;
        }
         
        .logtable td {
            word-break:break-all;
            word-wrap:break-word;
            vertical-align:top;
            text-align:left;
        }
 
        .logtable th {
            text-align:left;
        }
    </style>
    <body style=`"width:900px;margin-left:auto;margin-right:auto;`">
        <div>
            Generated on $Date
        </div>
        <div>
            <div>
                <label style=`"margin:0px;`">Domain:</label><span style=`"margin-left:5px;`">$Domain</span>
            </div>
            <div>
                <label style=`"margin:0px;`">Number Of Domain Controllers:</label><span style=`"margin-left:5px;`">$Count</span>
            </div>
        </div>
        <div>
"@


                foreach ($Property in (Get-Member -InputObject $Report -MemberType NoteProperty))
                {
                    $HtmlString += "<h2 style=`"text-align:center`">" + $Property.Name + "</h2>"

                    $Grouping = $Report | Select-Object -ExpandProperty $Property.Name | Group-Object -Property MachineName     

                    foreach($Group in $Grouping)
                    {
                        $HtmlString += "<h3 style=`"color:red;`">DOMAIN CONTROLLER: " + $Group.Name + "</h3><br />"
                        $HtmlString += "<table class=`"logtable`"><thead><tr><th class=`"timecolumn`">Time Created</th><th class=`"messagecolumn`">Message</th></tr></thead><tbody>"

                        foreach($LogEntry in $Group.Group)
                        {
                            $HtmlString += "<tr><td>" + $LogEntry.TimeCreated + "</td><td>" + $LogEntry.Message + "</td>"
                        }

                        $HtmlString += "</tbody></table>"
                    }
                }

                $HtmlString += "</div></body></html>"

                Write-Output -InputObject $HtmlString
                break
            }
            default {
                Write-Output -InputObject $Report
                break
            }
        }
    }

    End {        
    }
}

Function Get-ADObjectPropertyChangeInfo {
    <#
        .SYNOPSIS
            Finds when a particular object's property was last modified and where the change took place. Then it searches the logs on the server.
 
        .DESCRIPTION
            The cmdlet finds out when an object's property was changed and what server it occured on so those logs can be searched.
 
        .PARAMETER Identity
            The identity of the group, user, computer, OU, or container to find information about. This can be the SAM Account Name, Display Name, Name, DN, or CN.
 
        .PARAMETER Property
            The property to search on to find when it was changed.
 
        .PARAMETER Domain
            Optionally indicates the domain the object exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the Identity parameter. If the identity parameter is specified, the current user domain is used. If the identity parameter is not specified, and the current windows identity is
            the system account, the local computer domain is used, otherwise the current user domain is used.
 
            If the Identity parameter is specified with a domain\username or UPN value, this parameter is ignored.
 
        .PARAMETER GetLog
            Indicates whether to try and retrieve any available log files from the server matching the change.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .PARAMETER PassThru
            If specified, will return the log object and not just display data.
 
        .EXAMPLE
            Get-ADObjectPropertyChangeInfo -Identity "john.smith" -Property "UserAccountControl"
 
            Gets information about when and where the UAC property was changed for john.smith.
         
        .EXAMPLE
            Get-ADObjectPropertyChangeInfo -Identity "john.smith" -Property "displayName" -GetLog
 
            Gets infomration about when and where the displayName property was changed for john.smith and also attempts to retrieve the log entry associated with this change.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Diagnostics.Eventing.Reader.EventLogRecord[]
 
                This is returned when GetLog is specified on Windows 7/Server 2008 R2 and above.
 
            System.Diagnostics.EventLogEntry[]
 
                This is returned when GetLog is specified and the OS is Windows Vista and below.
 
            System.Management.Automation.PSObject
 
                This is returned when GetLog is not specified, it contains the originating time, originating server the change was logged on, and the value of the changed property.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/5/2017
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$Identity,

        [Parameter(Position = 1, Mandatory=$true)]
        [System.String]$Property,

        [Parameter(Position = 2)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [Switch]$GetLog,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin {
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Result = ConvertTo-ADObject -Identity $Identity -Domain $Domain @CredSplat

        $DC = Get-ADDomainController -DomainName $Result.Domain.DNSRoot -Discover

        if ($Result.Identity -ne $null)
        {
            $Results = @()
            
            [Microsoft.ActiveDirectory.Management.ADReplicationAttributeMetadata[]]$Data = Get-ADReplicationAttributeMetadata -Object $Result.Identity.DistinguishedName -ShowAllLinkedValues -Server "$($DC.Name).$($DC.Domain)" -Properties $Property @CredSplat

            if ($Data -ne $null -and $Data.Count -ge 1)
            {
                foreach ($Entry in $Data)
                {
                    [System.DateTime]$OriginatingTime = $Entry.LastOriginatingChangeTime

                    if (![System.String]::IsNullOrEmpty($Entry.LastOriginatingChangeDirectoryServerIdentity))
                    {
                        #The property looks like
                        #CN=NTDS Settings,CN=SERVERNAME,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=contoso,DC=com

                        $CN = $Entry.LastOriginatingChangeDirectoryServerIdentity.Trim("CN=NTDS Settings,CN=")
                        $Name = $CN.Substring(0, $CN.IndexOf(","))

                        try 
                        {
                            $OriginatingServer = Get-ADComputer -Identity $Name -Server $Result.Domain.DNSRoot @CredSplat | Select-Object -ExpandProperty DNSHostName
                        }
                        catch [Exception]
                        {
                            $OriginatingServer = $Entry.Server
                        }
                    }
                    else
                    {
                        [System.String]$OriginatingServer = $Entry.Server
                    }
                        
                    $Results += [PSObject]@{"OriginatingServer" = $OriginatingServer; "OriginatingTime" = $OriginatingTime; "Value" = $Entry.AttributeValue}
                }
            }
            else
            {
                Write-Warning -Message "No replication data found for property $Property."
            }
            
            if ($Results.Count -gt 0)
            {
                if ($GetLog)
                {
                    Write-Host -Object "Consider running this as a job, for example: `$Job = Start-Job -ScriptBlock {Get-UserObjectChangedLog -Identity `$args[0] -Property `$args[1] -GetLog} -ArgumentList @(`"$Identity`",`"$Property`")}" -ForegroundColor Yellow

                    foreach ($Item in $Results)
                    {
                        $OriginatingServer = $Item.OriginatingServer
                        $OriginatingTime = $Item.OriginatingTime
                        $Value = $Item.Value

                        Write-Host -Object ""
                        Write-Host -Object "Originating Server: $OriginatingServer"
                        Write-Host -Object "Originating Time: $OriginatingTime"
                        Write-Host -Object "Value: $Value"
                        Write-Host -Object ""

                        if (Test-Connection -ComputerName $Item.OriginatingServer -Count 1)
                        {
                            Write-Host -Object "Checking logs on $OriginatingServer for log associated with $Value, this could take awhile..."                           

                            #Event Id 4738 - A User Account Was Changed
                            #Event Id 5136 - A directory service object was modified
                            #Event Id 4742 - A computer account was changed

                            #Event Id 4737 - A security-enabled global group was changed.
                            #Event Id 4755 - A security-enabled universal group was changed
                            #Event Id 4735 - A security-enabled local group was changed

                            $EventIds = @()

                            switch ($Result.Identity.ObjectClass)
                            {
                                "group" {
                                    $EventIds += 4737
                                    $EventIds += 4755
                                    $EventIds += 4735
                                    break
                                }
                                "user" {
                                    $EventIds += 4738
                                    break
                                }
                                "computer" {
                                    $EventIds += 4742
                                    break
                                }
                                "organizationalUnit" {
                                    $EventIds += 5136
                                    break
                                }
                                "container" {
                                    $EventIds += 5136
                                    break
                                }
                            }

                            [System.Version]$Version = [System.Environment]::OSVersion.Version
                            [System.Decimal]$Version = [System.Decimal]::Parse("$($Version.Major).$($Version.Minor)")
                            
                            $Log = $null

                            if ($Version -ge 6.1)
                            {
                                #Specify EndTime because they start with the most recent log and work backwards
                                [System.Diagnostics.Eventing.Reader.EventLogRecord[]]$Log = Get-WinEvent -ComputerName $OriginatingServer -MaxEvents 1 -FilterHashtable @{LogName=@("Security");ID=$EventIds;EndTime=$OriginatingTime} -ErrorAction SilentlyContinue @CredSplat | Where-Object {$_.Message -like "*$($Result.Identity.Name)*"}
                            }
                            else
                            {
                                #Doesn't support credential parameter
                                [System.Diagnostics.EventLogEntry[]]$Log = Get-EventLog -LogName "Security" -ComputerName $OriginatingServer -Before $OriginatingTime.AddSeconds(1) -After $OriginatingTime.AddSeconds(-1) -ErrorAction SilentlyContinue -Cre | Where-Object {$_.EventID -in $EventIds -and $_.TimeGenerated -eq $OriginatingTime -and $_.Message -like "*$($Result.Identity.Name)*"} | Sort-Object -Descending -Property TimeGenerated
                            }

                            if ($Log -ne $null -and $Log.Count -ge 1)
                            {
                                if ($PassThru)
                                {
                                    Write-Output -InputObject $Log[0]    
                                }
                                else
                                {
                                    Write-Host -Object ($Log[0] | Format-List | Out-String)
                                }
                            }
                            else
                            {
                                Write-Warning -Message "No log files could be retrieved."
                            }
                        }
                        else
                        {
                            Write-Warning -Message "Could not contact $OriginatingServer to retrieve logs."
                        }
                    }
                }
                else
                {
                    if ($PassThru)
                    {
                        Write-Output -InputObject $Results
                    }
                    else
                    {
                        foreach ($Item in $Results)
                        {
                            Write-Host -Object ""
                            Write-Host -Object "Originating Server: $($Item.OriginatingServer)"
                            Write-Host -Object "Originating Time: $($Item.OriginatingTime)"
                            Write-Host -Object "Value: $($Item.Value)"
                            Write-Host -Object ""
                        }
                    }
                }               
            }
        }
        else
        {
            Write-Error -Message "Could not retrieve the specified principal, $Identity, from Active Directory."
        }
    }

    End {
    }
}

Function Get-ADGroupMembershipAddInfo {
    <#
        .SYNOPSIS
            Uses replication metadata to find when a member was added to a group.
 
        .DESCRIPTION
            The cmdlet gathers the replication metadata to find when a member was added to a group. This data can be used to search the logs on the identified Domain Controller to find who added the member.
 
        .PARAMETER GroupMember
            The Identity of the group member that was added. This can be the SAM Account Name, Display Name, Name, or CN.
 
        .PARAMETER Group
            The Identity of the group. This can be the SAM Account Name, Display Name, Name, or CN.
 
        .PARAMETER MemberDomain
            Optionally indicates the domain the group member exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the GroupMember parameter.
 
        .PARAMETER GroupDomain
            Optionally indicates the domain the group exists in. If this parameter is not specified, the domain of the group member is used.
 
        .PARAMETER GetLog
            Indicates whether to try and retrieve any available log files from the server matching the change. Defaults to true.
 
        .PARAMETER UseActiveDirectoryPowershell
            Uses the Active Directory Powershell module to retrieve replication metadata. The default selection.
 
        .PARAMETER UseRepAdmin
            Uses the repadmin command line function and parses the output.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .PARAMETER PassThru
            If specified, will return the log object and not just display data.
 
        .EXAMPLE
            Get-ADGroupMembershipAddInfo -GroupMember Administrator -Group "Account Operators" -GetLog
 
            Writes out the server and time the member was added and attempts to retrieve the log file from the specified server for additional information which is written out to the pipeline.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/5/2017
    #>

    [Alias("Get-WhenMemberAddedToGroup")]
    [CmdletBinding(DefaultParameterSetName="Powershell")]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$GroupMember,

        [Parameter(Position = 1, Mandatory = $true)]
        [System.String]$Group,

        [Parameter(Position = 2)]
        [System.String]$MemberDomain = [System.String]::Empty,

        [Parameter(Position = 3)]
        [System.String]$GroupDomain = [System.String]::Empty,

        [Parameter()]
        [Switch]$GetLog,

        [Parameter(ParameterSetName="Powershell")]
        [Switch]$UseActiveDirectoryPowershell,

        [Parameter(ParameterSetName="RepAdmin")]
        [Switch]$UseRepAdmin,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin {
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $ADGroupMemberResult = ConvertTo-ADObject -Identity $GroupMember -Domain $MemberDomain @CredSplat

        if ([System.String]::IsNullOrEmpty($GroupDomain))
        {
            $GroupDomain = $ADGroupMemberResult.Domain.DNSRoot
        }

        [Microsoft.ActiveDirectory.Management.ADGroup]$ADGroup = Get-ADGroup -Identity $Group -Server $GroupDomain @CredSplat


        if ($ADGroup -ne $null)
        {
            Write-Verbose -Message "Found AD group $($ADGroup.Name)"

            $DC = Get-ADDomainController -DomainName $GroupDomain -Discover

            Write-Verbose -Message "Using $($DC.Name).$($DC.Domain) as the initial domain controller."

            if ($ADGroupMemberResult.Identity -ne $null)
            {
                Write-Verbose -Message "Found group member $($ADGroupMemberResult.Identity.DistinguishedName)"

                switch ($PSCmdlet.ParameterSetName)
                {    
                    "RepAdmin"
                    {
                        $Data = repadmin /showmeta $ADGroup.Name $ADGroupMemberResult.Domain.DNSRoot

                        #List only returns the first match
                        #Context specifies the number of lines before and after that are returned around the matching line
                        [System.String]$Line = $Data | Select-String -Pattern $ADGroupMember.Identity.Name -List -Context 2,0 
                        $Content = $Line.Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)
                        [System.String]$OriginatingServer = $Content[4].Substring($Content[4].IndexOf("\") + 1).Trim()
                        [System.DateTime]$OriginatingTime = Get-Date($Content[2] + " " + $Content[3])

                        break
                    }
                    "Powershell"
                    {
                        [Microsoft.ActiveDirectory.Management.ADReplicationAttributeMetadata]$Data = `
                            Get-ADReplicationAttributeMetadata -Object $ADGroup.DistinguishedName -ShowAllLinkedValues -Server "$($DC.Name).$($DC.Domain)" -Properties member @CredSplat | `
                            Where-Object {$_.AttributeValue -eq $ADGroupMemberResult.Identity.DistinguishedName} | `
                            Sort-Object -Descending -Property "LastOriginatingChangeTime" | `
                            Select-Object -First 1 

                        if ($Data -ne $null)
                        {
                            [System.DateTime]$OriginatingTime = $Data.LastOriginatingChangeTime

                            if (![System.String]::IsNullOrEmpty($Data.LastOriginatingChangeDirectoryServerIdentity))
                            {
                                $CN = $Data.LastOriginatingChangeDirectoryServerIdentity.Trim("CN=NTDS Settings,CN=")
                                $Name = $CN.Substring(0, $CN.IndexOf(","))

                                try 
                                {
                                    [System.String]$OriginatingServer = Get-ADComputer -Identity $Name -ErrorAction Stop @CredSplat | Select-Object -ExpandProperty DNSHostName
                                }
                                catch [Exception]
                                {
                                    [System.String]$OriginatingServer = $Data.Server
                                }
                            }
                            else
                            {
                                [System.String]$OriginatingServer = $Data.Server
                            }
                        }

                        break
                    }
                    default
                    {
                        throw "Could not determine parameter set."
                    }
                }

                if ($Data -ne $null)
                {
                    Write-Host -Object "Originating Server: $OriginatingServer"
                    Write-Host -Object "Originating Time: $OriginatingTime"

                    if ($GetLog)
                    {
                        if (Test-Connection -ComputerName $OriginatingServer -Count 1 @CredSplat)
                        {
                            Write-Host -Object "Checking logs on $OriginatingServer, this could take awhile..."
                            Write-Host -Object "Consider running this as a job: `$Job = Start-Job -ScriptBlock {Get-WhenMemberAddedToGroup -Group `$args[0] -GroupMember `$args[1]} -ArgumentList `"$Group`",`"$GroupMember`"" -ForegroundColor Yellow

                            # Event Id 4732, 636 - Member added to a domain local group
                            # Event Id 4728, 632 - Member added to a global group
                            # Event Id 4756, 660 - Member added to a universal group

                            $EventIds = @(4732,4728,4756,636,632,660)

                            $Version = [System.Environment]::OSVersion.Version
                            [System.Decimal]$Version = [System.Decimal]::Parse("$($Version.Major).$($Version.Minor)")
                            
                            $Log = $null

                            if ($Version -ge 6.1)
                            {                     
                                [System.Diagnostics.Eventing.Reader.EventLogRecord[]]$Log = Get-WinEvent -ComputerName $OriginatingServer -MaxEvents 1 -FilterHashtable @{LogName=@("Security");ID=$EventIds;StartTime=$OriginatingTime} -ErrorAction SilentlyContinue @CredSplat | Where-Object {$_.Message -like "*$($ADGroupMemberResult.Identity.Name)*"} 
                            }
                            else
                            {
                                [System.Diagnostics.EventLogEntry[]]$Log = Get-EventLog -LogName "Security" -ComputerName $OriginatingServer -Before $OriginatingTime.AddSeconds(1) -After $OriginatingTime.AddSeconds(-1) -ErrorAction SilentlyContinue | Where-Object {$_.EventID -in $EventIds -and $_.TimeGenerated -eq $OriginatingTime -and $_.Message -like "*$ADGroupMember*"}
                            }

                            if ($Log -ne $null -and $Log.Count -ge 1)
                            {
                                if ($PassThru)
                                {
                                    Write-Output -InputObject $Log[0]    
                                }
                                else
                                {
                                    Write-Host -Object ($Log[0] | Format-List | Out-String)
                                }
                            }
                            else
                            {
                                Write-Warning -Message "No log files could be retrieved."
                            }
                        }
                        else
                        {
                            Write-Warning -Message "Could not contact $OriginatingServer to retrieve logs."
                        }
                    }
                    else
                    {
                        if ($PassThru) 
                        {
                            Write-Output -InputObject (New-Object PSObject -Property @{"Originating Server"=$OriginatingServer;"Originating Time"=$OriginatingTime})
                        }
                    }
                }
                else
                {
                    Write-Warning -Message "No replication data found for Object $($ADGroup.Name) and Property 'member'."
                }
            }
            else
            {
                Write-Error -Message "Could not find the specified group member: $GroupMember"
            }
        }
        else
        {
            Write-Error -Message "Could not find the specified group: $Group"
        }
    }

    End    {        
    }
}

Function Get-ADPrincipalDetails {
    <#
        .SYNOPSIS
            Gets details about a specific AD principal.
 
        .DESCRIPTION
            Gets information about an AD principal to include the principals DistinguishedName, whether it has Domain or Enterprise admin rights, and its complete group membership.
 
            This cmdlet is used by the Test-IsDomainAdmin, Test-IsEnterpriseAdmin, and Test-IsEnterpriseOrDomainAdmin, but could be useful for other purposes.
 
            Domain controllers are reported as having Domain Admin privileges.
 
        .EXAMPLE
            Get-ADPrincipalDetails -Identity "Domain Admins" -Domain contoso
 
            Gets information about the Domain Admin group
 
        .PARAMETER Identity
            The identity of the AD principal to gather details on. This is a string that is matched against AD object properties:
 
                -Name
                -SamAccountName
                -CN
                -DistinguishedName
                -Display Name
                -ObjectSID
                -ObjectGUID
 
            For user objects, the identity can be provided as a User Principal Name (UPN) or domain\username format. In these cases, the domain will be extracted
            from the identity and does not need to be specified.
 
            If this parameter is not specified, the current windows identity principal is utilized, which could be a user or system account, which is translated to the corresponding
            computer object.
 
        .PARAMETER Domain
            Optionally indicates the domain the object exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the Identity parameter. If the identity parameter is specified, the current user domain is used. If the identity parameter is not specified, and the current windows identity is
            the system account, the local computer domain is used, otherwise the current user domain is used.
 
            If the Identity parameter is specified with a domain\username or UPN value, this parameter is ignored.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeLine = $true, Mandatory= $true)]
        [AllowEmptyString()]
        [System.String]$Identity = [System.String]::Empty,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        [System.Boolean]$DomainAdmin = $false
        [System.Boolean]$EnterpriseAdmin = $false

        $Result = ConvertTo-ADObject -Identity $Identity -Domain $Domain @CredSplat

        if ($Result.Identity -ne $null)
        {
            #If the computer is a domain controller, it has domain admin rights under the SYSTEM account
            if ($Result.Identity.ObjectClass -eq "computer")
            {
                $DNs = Get-ADDomainController -Filter * @CredSplat | Select-Object -ExpandProperty ComputerObjectDN

                if ($Result.Identity.DistinguishedName -in $DNs)
                {
                    $DomainAdmin = $true

                    [Microsoft.ActiveDirectory.Management.ADForest]$Forest = Get-ADForest -Identity $Result.Forest.DNSRoot

                    if ($Result.Domain.DNSRoot -eq $Forest.RootDomain)
                    {
                        #The DC is in the forest root domain, so it has enterprise admin rights
                        $EnterpriseAdmin = $true
                    }
                }
            }

            #Only do a group membership review if we haven't determined this was an enterprise DC
            if ($DomainAdmin -ne $true -or $EnterpriseAdmin -ne $true)
            {
                [Microsoft.ActiveDirectory.Management.ADGroup[]]$Groups = Get-ADPrincipalGroupMembership -Identity $Result.Identity.DistinguishedName -Domain $Result.Domain.DNSRoot @CredSplat

                if ($Result.Identity.ObjectClass -eq "group")
                {
                    [Microsoft.ActiveDirectory.Management.ADGroup]$Group = Get-ADGroup -Identity $Result.Identity.DistinguishedName -Server $Result.Domain.DNSRoot @CredSplat
                    $Groups += $Group
                }

                [Security.Principal.SecurityIdentifier]$DomainAdminSID = New-Object -TypeName System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::AccountDomainAdminsSid, $Result.Domain.DomainSID)      
                [Security.Principal.SecurityIdentifier]$EnterpriseAdminSID = New-Object -TypeName System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::AccountEnterpriseAdminsSid, $Result.Forest.DomainSID )

                foreach ($Group in $Groups)
                {
                    Write-Verbose -Message "Evaluating $($Group.DistinguishedName) for SID"
                    if ($Group.SID -eq $EnterpriseAdminSID) 
                    {
                        $EnterpriseAdmin = $true
                    }
                    elseif ($Group.SID -eq $DomainAdminSID)
                    {
                        $DomainAdmin = $true
                    }
                
                    #Since both have been found, don't need to review the rest of the groups
                    if ($DomainAdmin -eq $true -and $EnterpriseAdmin -eq $true)
                    {
                        break
                    } 
                }
            }

            Write-Output -InputObject ([PSCustomObject]@{Identity = $Result.Identity.DistinguishedName; EnterpriseAdmin = $EnterpriseAdmin; DomainAdmin = $DomainAdmin; Domain = $Result.Domain.DNSRoot; Forest = $Result.Forest.DNSRoot; Groups = $Groups})
        }
        else
        {
            throw [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectExistsException]"Could not find $Identity"
        }
    }

    End {
    }
}

Function Get-ADPrincipalGroupMembership {
    <#
        .SYNOPSIS
            Gets the complete group membership of an AD principal.
 
        .DESCRIPTION
            The Get-ADPrincipalGroupMembership gets all nested group membership of an AD principal. The principal can be a user, computer, or group.
 
        .EXAMPLE
            Get-ADPrincipalGroupMembership -Identity Administrator
 
            Gets all group membership for the Administrator account.
 
        .EXAMPLE
            Get-ADPrincipalGroupMembership -Identity "Domain Admins -Domain root
 
            Gets all group membership for the Domain Admins group in the root domain. This command could be run from a different domain that also
            had a group named "Domain Admins".
 
        .PARAMETER Identity
            The identity of the AD principal to get group membership for. This is a string that is matched against AD object properties:
 
                -Name
                -SamAccountName
                -CN
                -DistinguishedName
                -Display Name
                -ObjectSID
                -ObjectGUID
 
            For user objects, the identity can be provided as a User Principal Name (UPN) or domain\username format. In these cases, the domain will be extracted
            from the identity and does not need to be specified.
 
            If this parameter is not specified, the current windows identity principal is utilized, which could be a user or system account, which is translated to the corresponding
            computer object.
 
        .PARAMETER Domain
            Optionally indicates the domain the object exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the Identity parameter. If the identity parameter is specified, the current user domain is used. If the identity parameter is not specified, and the current windows identity is
            the system account, the local computer domain is used, otherwise the current user domain is used.
 
            If the Identity parameter is specified with a domain\username or UPN value, this parameter is ignored.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADGroup[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>

    Param(
        [Parameter(ValueFromPipeline = $true, Position = 0)]
        [System.String]$Identity,

        [Parameter(Position = 1)]
        [System.String]$Domain,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Result = ConvertTo-ADObject -Identity $Identity -Domain $Domain @CredSplat
        
        #The groups array will contain the final set of groups
        $Groups = @()

        #The queue will hold the groups that need to be evaluated in the nested hierarchy
        [System.Collections.Queue]$GroupsToCheck = New-Object -TypeName System.Collections.Queue

        Write-Verbose -Message "Evaluating $($Result.Identity.DistinguishedName)"

        #Get the group membership of the evaluated principal
        #Use the memberOf property because Get-ADPrincipalGroupMembership doesn't accept all object classes like foreignSecurityPrincipal

        [Microsoft.ActiveDirectory.Management.ADGroup[]]$NewGroups = $Result.Identity.memberOf | ForEach-Object {
            $QueryDomain = $_.Substring($_.IndexOf("DC=")).Replace("DC=", "").Replace(",",".")

            Write-Output -InputObject (Get-ADGroup -Identity $_ -Server $QueryDomain @CredSplat)
        }

        #Add the immediate group membership to the groups to check and the discovered groups
        if ($NewGroups.Count -gt 0)
        {
            foreach ($Group in $NewGroups)
            {
                Write-Verbose -Message "`tMember Of: $Group"
                Write-Verbose -Message "`t`tAdding to groups"
                $GroupsToCheck.Enqueue($Group)
                $Groups += $Group
            }
        }
        else
        {
            Write-Verbose -Message "`tNo parent groups."
        }

        #While there are groups to check, get the group membership of each group
        while ($GroupsToCheck.Count -gt 0)
        {
            [Microsoft.ActiveDirectory.Management.ADGroup]$Group = $GroupsToCheck.Dequeue()
            Write-Verbose -Message "Evaluating $Group"

            $QueryDomain = $Group.DistinguishedName.Substring($Group.DistinguishedName.IndexOf("DC=")).Replace("DC=", "").Replace(",",".")

            [Microsoft.ActiveDirectory.Management.ADGroup[]]$NewGroups = Get-ADPrincipalGroupMembership -Identity $Group -Server $QueryDomain -ErrorAction SilentlyContinue @CredSplat

            #If the groups array doesn't have the group yet, add it
            #There may be group memberships where two groups are both members of the same parent group, or there may be
            #circular nesting where A is a member of B, B is a member of C, and C is a member of A
            if ($NewGroups.Count -gt 0)
            {
                foreach ($Group in $NewGroups)
                {
                    Write-Verbose -Message "`tMember Of: $Group"
                    if (($Groups | Select-Object -ExpandProperty DistinguishedName) -notcontains $Group.DistinguishedName)
                    {
                        Write-Verbose -Message "`t`tAdding to groups."
                        $GroupsToCheck.Enqueue($Group)
                        $Groups += $Group
                    }
                    else
                    {
                        Write-Verbose -Message "`t`tAlready discovered."
                    }
                }
            }
            else
            {
                Write-Verbose -Message "`tNo parent groups."
            }
        }

        #Return the updated total group membership
        Write-Output -InputObject $Groups
    }

    End {
    }
}

Function Get-ADGroupMembers {
<#
        .SYNOPSIS
            Gets the complete group membership of an AD group.
 
        .DESCRIPTION
            The Get-ADNestedGroupMembers gets all nested group members of an AD group.
 
        .PARAMETER Identity
            The identity of the AD group to get group members of. This is a string that is matched against AD object properties:
 
                -Name
                -SamAccountName
                -CN
                -DistinguishedName
                -Display Name
                -ObjectSID
                -ObjectGUID
 
 
        .PARAMETER Domain
            Optionally indicates the domain the object exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the Identity parameter.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Get-ADGroupMembers -Identity Administrators
 
            Gets all group members in the Administrators group.
 
        .EXAMPLE
            Get-ADGroupMembers -Identity "Domain Admins" -Domain root
 
            Gets all group members for the Domain Admins group in the root domain. This command could be run from a different domain that also
            had a group named "Domain Admins".
 
        .INPUTS
            System.String
 
        .OUTPUTS
            Microsoft.ActiveDirectory.Management.ADObject[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>

    Param(
        [Parameter(ValueFromPipeline = $true, Position = 0)]
        [System.String]$Identity,

        [Parameter(Position = 1)]
        [System.String]$Domain,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Result = ConvertTo-ADObject -Identity $Identity -Domain $Domain @CredSplat

        if ($Result.Identity.ObjectClass -ieq "group")
        {
            # This will hold all of the groups we need to evaluate the membership of to find nested members
            [Microsoft.ActiveDirectory.Management.ADObject[]]$Groups = @()
            [System.String[]]$GroupsStr = @()

            # This is the top level group we are evaluating
            [Microsoft.ActiveDirectory.Management.ADGroup]$Group = Get-ADGroup -Identity $Result.Identity.DistinguishedName -Properties Members @CredSplat

            # These are the direct level members of the group
            [Microsoft.ActiveDirectory.Management.ADObject[]]$Members = $Group.Members | Get-ADObject @CredSplat
            $GroupsStr += $Members | Select-Object -ExpandProperty DistinguishedName

            # Keep track of just the DNs of the discovered members to make it easy to see if we've added a user or group to the total members list already
            [System.String[]]$MemberDNs = $Members | Select-Object -ExpandProperty DistinguishedName

            # Populate the groups array with first level members of the group that are also groups
            $Groups += $Members | Where-Object {$_.ObjectClass -ieq "group"}

            while ($Groups.Length -gt 0)
            {
                # This will hold newly found groups in the child groups being evaluated in the foreach loop
                [Microsoft.ActiveDirectory.Management.ADObject[]]$NewGroups = @()
                
                foreach ($Grp in $Groups)
                {
                    # Get the group so we can get the members property, we can't get this on an ADObject
                    [Microsoft.ActiveDirectory.Management.ADGroup]$MemberGroup = Get-ADGroup -Identity $Grp.DistinguishedName -Properties Members @CredSplat
                    
                    # Retrieve the direct members of this group
                    [Microsoft.ActiveDirectory.Management.ADObject[]]$NewMembers = $MemberGroup.Members | Get-ADObject @CredSplat

                    # Add newly found groups to the new groups list if they aren't members yet (otherwise we've already evaluated their members)
                    $NewGroups += $NewMembers | Where-Object {$_.ObjectClass -ieq "group" -and $MemberDNs -inotcontains $_.DistinguishedName}

                    # Add the current group we're evaluating to the new members list so we can include it in the iteration below
                    $NewMembers += (Get-ADObject -Identity $MemberGroup.DistinguishedName @CredSplat)

                    # Check each newly discovered member to see if they have already been added to the total membership list (they might have
                    # from being a member in another group that was also a child of the parent)
                    foreach ($NewMember in $NewMembers)
                    {
                        if ($MemberDNs -inotcontains $NewMember.DistinguishedName)
                        {
                            Write-Verbose -Message "Adding $($NewMember.DistinguishedName) to membership."
                            $Members += $NewMember
                        }
                    }

                    # Update the member DNs here so that in case two groups in the foreach loop both contain the same members, they won't
                    # get added twice
                    $MemberDNs = $Members | Select-Object -ExpandProperty DistinguishedName
                }

                # Update the groups array with any newly found groups
                $Groups = $NewGroups
                
            }

            Write-Output -InputObject $Members
        }
        else
        {
            throw "Provided identity must be a security or distribution group."
        }
    }

    End {
    }
}

Function Get-OldADUsers {
    <#
        .SYNOPSIS
            Get users that haven't logged in for the specified time.
 
        .DESCRIPTION
            The cmdlet finds all users that haven't logged in for the specified time. It first looks for the msDS-LastSuccessfulInteractiveLogon if that has been enabled in the domain. If it is not present, it uses the LastLogonTimeStamp.
 
        .PARAMETER OlderThan
            The number of days since the last logon.
 
        .PARAMETER SearchBase
            The DistinguishedName of the OU path to search under. This defaults to the root of the domain.
 
        .PARAMETER Domain
            The domain to search in.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .EXAMPLE
            Get-OldUsers -DaysOld 60
             
            Gets all users that haven't logged on in at least 60 days.
 
        .EXAMPLE
            Get-OldUsers -DaysOld 60 -SearchBase "OU=Finance,OU=HQ,DC=contoso,DC=com"
 
            Gets all users in the Finance OU and child OUs that haven't logged on in at least 60 days.
 
        .INPUTS
            System.Int32
 
        .OUTPUTS
            System.Management.Automation.PSObject[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeLine = $true)]
        [System.Int32]$DaysOld,

        [Parameter(Position = 1)]
        [System.String]$SearchBase = [System.String]::Empty,

        [Parameter(Position = 2)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {
    }

    Process
    {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Result = @()

        if ([System.String]::IsNullOrEmpty($Domain))
        {
            $Domain = $env:USERDOMAIN
        }
        
        [Microsoft.ActiveDirectory.Management.ADDomain]$ADDomain = Get-ADDomain -Identity $Domain @CredSplat

        if ($SearchBase -eq [System.String]::Empty)
        {
            $SearchBase = $ADDomain.DistinguishedName
        }

        $Users = Get-ADUser -Filter * -Property UserAccountControl,"msDS-LastSuccessfulInteractiveLogonTime",LastLogonTimeStamp,LastLogon -SearchBase $SearchBase -Server $ADDomain.DNSRoot @CredSplat |
            Select-Object -Property DistinguishedName,Enabled,LastLogon,LastLogonTimeStamp,"msDS-LastSuccessfulInteractiveLogonTime",UserAccountControl,SamAccountName

        foreach ($User in $Users)
        {
            Write-Verbose -Message "Reviewing user $($User.DistinguishedName)"
            $UserObject = New-Object -TypeName PSObject -Property @{"Days Since Last Logon"= 0; UserName = $User.DistinguishedName; "Last Logon" = $null; UAC = $User.UserAccountControl}

            if ($User."msDS-LastSuccessfulInteractiveLogonTime" -ne $null -and $User."msDS-LastSuccessfulInteractiveLogonTime" -ne [System.String]::Empty -and 
                $User."msDS-LastSuccessfulInteractiveLogonTime" -ne 0)
            {
                Write-Verbose -Message "Last interactive logon $($User."msDS-LastSuccessfulInteractiveLogonTime")"
                $UserObject."Last Logon" = (Get-Date -Date $User."msDS-LastSuccessfulInteractiveLogonTime").AddYears(1600) 
            }
            elseif ($User.LastLogonTimeStamp -ne $null -and $User.LastLogonTimeStamp -ne 0)
            {
                Write-Verbose -Message "Last logon timestamp $($User.LastLogonTimeStamp)"
                $UserObject."Last Logon" = (Get-Date -Date $User.LastLogonTimeStamp).AddYears(1600)
            }
            elseif ($User.LastLogon -ne $null -and $User.LastLogon -ne 0)
            {
                Write-Verbose -Message "Last logon $($User.LastLogon)"
                $UserObject."Last Logon" = (Get-Date -Date $User.LastLogon).AddYears(1600)
            }
            else 
            {
                Write-Verbose -Message "User object has no logon information"
                $UserObject."Last Logon" = [System.DateTime]::MinValue
            }

            Write-Verbose -Message "Last logon $($UserObject."Last Logon")"

            $UserObject."Days Since Last Logon" = ((Get-Date).ToUniversalTime() - [System.DateTime]($UserObject."Last Logon")).TotalDays

            Write-Verbose -Message "Days since last logon $($UserObject."Days Since Last Logon")"

            if ($UserObject."Days Since Last Logon" -ge $DaysOld)
            {
                $Result += $UserObject
            }

            Write-Verbose -Message "----------"
        }

        Write-Output -InputObject $Result
    }

    End {
    }
}

Function Get-ADUserAccountControl {
    <#
        .SYNOPSIS
            Gets an enumeration of a user's UserAccountControl attribute.
 
        .DESCRIPTION
            The Get-ADUserAccountControl cmdlet gets the value from a user's UserAccountControl attribute and enumerates it to the different parts that create the bitwise value.
 
        .EXAMPLE
            Get-ADUserAccountControl -Identity "John Smith"
 
            Gets the UAC enumeration for John Smith.
 
        .PARAMETER Identity
            The identity of the object, can be a SamAccountName, DistinguishedName, Name, Display Name, CN, ObjectSID, ObjectGUID or DisplayName property.
 
        .PARAMETER Domain
            Optionally indicates the domain the object exists in. If this parameter is not specified, the domain of the local user or computer is used, depending on what is specified for
            the Identity parameter. If the identity parameter is specified, the current user domain is used. If the identity parameter is not specified, and the current windows identity is
            the system account, the local computer domain is used, otherwise the current user domain is used.
 
            If the Identity parameter is specified with a domain\username or UPN value, this parameter is ignored.
 
        .PARAMETER Credential
            Specifies a user account that has permission to perform this action. The default is the current user.
 
            Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/7/2017
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [System.String]$Identity,

        [Parameter(Position = 1)]
        [System.String]$Domain = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    Begin {        
    }

    Process {
        $CredSplat = @{}

        if ($Credential -ne [PSCredential]::Empty)
        {
            $CredSplat["Credential"] = $Credential
        }

        $Result = ConvertTo-ADObject -Identity $Identity -Domain $Domain @CredSplat

        if ($Result.Identity -ne $null)
        {
            if ($Result.Identity.ObjectClass -in @("user", "computer"))
            {
                $UAC = $Result.Identity.UserAccountControl

                $Matches = @()
                foreach ($Value in $script:UACValues) {
                    #Perform bitwise 'and' to compare the current UAC against each possible value and record matches
                    if ($UAC -band [System.Convert]::ToInt64($Value.Key, 16)) {
                        $Matches += $Value
                    }
                }

                Write-Output -InputObject $Matches
            }
            else
            {
                Write-Error -Message "The identity must be either a user or computer object."
            }
        }
        else
        {
            Write-Error -Message "Could not find $Identity"
        }
    }

    End {        
    }
}

Function Get-ADComputerSite {
    <#
        .SYNOPSIS
            Gets the AD Site for the specified computer.
 
        .DESCRIPTION
            Calls DsGetSiteName() and retrieves the current Active Directory site for the specified ComputerName. The cmdlet will throw an exception if it cannot
            contact the specified computer or if the computer is not part of a site.
 
        .PARAMETER ComputerName
            The computer to query the AD site name for. Leave this blank to query the local machine.
 
        .PARAMETER AsADSite
            This parameter specifies that the site name returned to DsGetSiteName is supplied to Get-ADReplicationSite and returns a Microsoft.ActiveDirectory.Management.ADReplicationSite instead of a string.
 
        .EXAMPLE
            $SiteName = Get-ADComputerSite
 
            Gets the AD site for the local computer. The return value would be Default-First-Site-Name if the computer is in that AD site.
 
        .EXAMPLE
            $SiteName = Get-ADComputerSite -ComputerName myserver01
 
            Gets the AD site for the remote computer myserver01.
 
        .EXAMPLE
            [Microsoft.ActiveDirectory.Management.ADReplicationSite]$ReplSite = Get-ADComputerSite -AsADSite
 
            Gets the AD site the local computer is a member of as an Microsoft.ActiveDirectory.Management.ADReplicationSite object.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.String, Microsoft.ActiveDirectory.Management.ADReplicationSite
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/7/2017
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter()]
        [ValidateNotNull()]
        [System.String]$ComputerName = [System.String]::Empty,

        [Parameter()]
        [Switch]$AsADSite
    )

    Begin {
        Add-Type -TypeDefinition $script:AdSite
    }

    Process {
        [System.String]$SiteName = [NetApi32]::DsGetSiteName($ComputerName)

        if ($AsADSite)
        {
            Write-Output -InputObject (Get-ADReplicationSite -Identity $SiteName)
        }
        else {
            Write-Output -InputObject $SiteName
        }
    }

    End {
    }
}

$script:UACValues = @(
            [PSCustomObject]@{Key="0x00000001";Value="ADS_UF_SCRIPT"},
            [PSCustomObject]@{Key="0x00000002";Value="ADS_UF_ACCOUNT_DISABLE"},
            [PSCustomObject]@{Key="0x00000008";Value="ADS_UF_HOMEDIR_REQUIRED"},
            [PSCustomObject]@{Key="0x00000010";Value="ADS_UF_LOCKOUT"}
            [PSCustomObject]@{Key="0x00000020";Value="ADS_UF_PASSWD_NOTREQD"},
            [PSCustomObject]@{Key="0x00000040";Value="ADS_UF_PASSWD_CANT_CHANGE"},
            [PSCustomObject]@{Key="0x00000080";Value="ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED"},
            [PSCustomObject]@{Key="0x00000100";Value="ADS_UF_TEMP_DUPLICATE_ACCOUNT"},
            [PSCustomObject]@{Key="0x00000200";Value="ADS_UF_NORMAL_ACCOUNT"},
            [PSCustomObject]@{Key="0x00000800";Value="ADS_UF_INTERDOMAIN_TRUST_ACCOUNT"},
            [PSCustomObject]@{Key="0x00001000";Value="ADS_UF_WORKSTATION_TRUST_ACCOUNT"},
            [PSCustomObject]@{Key="0x00002000";Value="ADS_UF_SERVER_TRUST_ACCOUNT"},
            [PSCustomObject]@{Key="0x00010000";Value="ADS_UF_DONT_EXPIRE_PASSWD"},
            [PSCustomObject]@{Key="0x00020000";Value="ADS_UF_MNS_LOGON_ACCOUNT"},
            [PSCustomObject]@{Key="0x00040000";Value="ADS_UF_SMARTCARD_REQUIRED"},
            [PSCustomObject]@{Key="0x00080000";Value="ADS_UF_TRUSTED_FOR_DELEGATION"},
            [PSCustomObject]@{Key="0x00100000";Value="ADS_UF_NOT_DELEGATED"},
            [PSCustomObject]@{Key="0x00200000";Value="ADS_UF_USE_DES_KEY_ONLY"},
            [PSCustomObject]@{Key="0x00400000";Value="ADS_UF_DONT_REQUIRE_PREAUTH"},
            [PSCustomObject]@{Key="0x00800000";Value="ADS_UF_PASSWORD_EXPIRED"},
            [PSCustomObject]@{Key="0x01000000";Value="ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION"},
            [PSCustomObject]@{Key="0x02000000";Value="ADS_UF_NO_AUTH_DATA_REQUIRED"},
            [PSCustomObject]@{Key="0x04000000";Value="ADS_UF_PARTIAL_SECRETS_ACCOUNT"}
        )

$script:AdSite = @"
using System;
using System.Runtime.InteropServices;
 
public static class NetApi32
{
    [DllImport("NetApi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern UInt32 DsGetSiteName([MarshalAs(UnmanagedType.LPWStr)]string ComputerName, out IntPtr SiteNameBuffer);
 
    [DllImport("NetApi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern int NetApiBufferFree(IntPtr Buffer);
 
    private static void ThrowLastWin32Error()
    {
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
    }
 
    public static string DsGetSiteName()
    {
        return DsGetSiteName(String.Empty);
 
        // This only works for the local system
        // [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite();
    }
 
    public static string DsGetSiteName(string ComputerName)
    {
        IntPtr SiteNameBuffer = IntPtr.Zero;
        UInt32 HResult = DsGetSiteName(ComputerName, out SiteNameBuffer);
        string SiteName = Marshal.PtrToStringAuto(SiteNameBuffer);
        NetApiBufferFree(SiteNameBuffer);
 
        if (HResult != 0)
        {
            if (HResult == 0x00000005)
            {
                throw new Exception(String.Format("ERROR_ACCESS_DENIED : The function could not access the computer {0}.", ComputerName));
            }
            else if (HResult == 0x00000008)
            {
                throw new Exception("ERROR_NOT_ENOUGH_MEMORY : Not enough memory.");
            }
            else if (HResult == 0x0000077F)
            {
                throw new Exception("ERROR_NO_SITENAME : The computer is not assigned to a site.");
            }
            else if (HResult == 0x000006BA)
            {
                throw new Exception(String.Format("RPC_S_SERVER_UNAVAILABLE : ComputerName {0} not found.", ComputerName));
            }
            else
            {
                ThrowLastWin32Error();
            }
        }
 
        return SiteName;
    }
}
"@