Find-SPNs.ps1


Description

@GitHub Copilot - Welcome @BanterBoy, I’m your Copilot and I’m here to help you get things done faster. I can identify issues, explain and even improve code.

You can ask generic questions, but what I’m really good at is helping you with your code. For example:

Generate unit tests for my code Explain the selected code Propose a fix for the bugs in my code If you want to learn more about my capabilities and limitations, check out the Copilot documentation.

I’m powered by AI, so surprises and mistakes are possible. Make sure to verify any generated code or suggestions, and share feedback so that we can learn and improve.

@BanterBoy: - Explain what the script does and why it is useful.

@GitHub Copilot -


Script

<#
.SYNOPSIS
    NAME Find-SPNs.ps1
    This script provides a list of objects configured with a provided Service Principal Name (SPN) or search for all
    duplicate SPNs in the Active Directory Forest.

.DESCRIPTION
	This script provides a list of objects configured with a provided Service Principal Name (SPN) or search for all
    duplicate SPNs in the Active Directory Forest.

	if there are significant errors, review the other log files with the same date and time in
	the name in c:\temp\logs.

.PARAMETER SPNName
    The script searches the Active Directory forest for this SPN value and displays the result.
	ALIASES: SPN
    Example: Find-SPNs.ps1 -SPNName "http/www.domain.com"

.PARAMETER UPNName
    The script searches the Active Directory forest for this UPN value and displays the result.
	ALIASES: UPN
    Example: Find-SPNs.ps1 -UPNName "[email protected]"

.PARAMETER FindDuplicateSPNs
    The script searches the Active Directory forest for this SPN value and displays the result.
	ALIASES: DuplicateSPNs, FDS
    Example: Find-SPNs.ps1 -FindDuplicateSPNs

.PARAMETER FindDuplicateUPNs
    The script searches the Active Directory forest for this UPN value and displays the result.
	ALIASES: DuplicateUPNs, FDU
    Example: Find-SPNs.ps1 -FindDuplicateUPNs

.PARAMETER Verbose
    The logging mode the script runs in.
    Example: Find-SPNs.ps1 -Verbose

.PARAMETER Debug
    Enables debug logging.
    Example: Find-SPNs.ps1 -Debug

.EXAMPLE
    List all objects with a specific configured SPN.
	PS C:\> c:\scripts\Find-SPNs.ps1 -SPNName "http/www.domain.com"

.EXAMPLE
    Find all duplicate SPNs in the forest.
    PS C:\> c:\scripts\Find-SPNs.ps1 -FindDuplicateSPNs

.EXAMPLE
    List all objects with a specific configured UPN.
	PS C:\> c:\scripts\Find-SPNs.ps1 -UPNName "[email protected]"

.EXAMPLE
    Find all duplicate UPNs in the forest.
    PS C:\> c:\scripts\Find-SPNs.ps1 -FindDuplicateUPNs


.NOTES
	NAME: Find-SPNs.ps1
 	AUTHOR: Sean Metcalf
 	AUTHOR EMAIL: [email protected]
 	CREATION DATE: 03/12/2012
	LAST MODifIED DATE: 03/19/2012
 	LAST MODifIED BY: Sean Metcalf
 	INTERNAL VERSION: 01.12.03.19.13
	RELEASE VERSION: 0.1.7
   ### Version Info Also Displays At Run-Time ###

    VERSION LOG
        * 03/12/2012: Initial Script Creation providing duplicate SPN reporting
		* 03/13/2012: Added capability to list all objects configured with a specific SPN.
                    - Add capability to list all objects configured with a specific UPN
                    - Find duplicate UPNs in the AD Forest.
        * 03/14/2012: Added wildcard support for SPN & UPN searching
		* 03/19/2012: Fixed LocalGC discovery & added parameter TargetGC

#>

# This Powershell script leverages some features only available with Powershell version 2.0.
# As such, there is no guarantee it will work with earlier versions of Powershell.
# Requires -Version 2.0

#####################
# Script Parameters #
#####################
Param
(
    [alias("LocalGC", "GC", "GCName")]
    [string] $TargetGC,

    [alias("SPN", "FindSPN")]
    [string] $SPNName,

    [alias("UPN", "FindUPN")]
    [string] $UPNName,

    [alias("DuplicateSPNs", "FDS")]
    [switch] $FindDuplicateSPNs,

    [alias("DuplicateUPNs", "FDU")]
    [switch] $FindDuplicateUPNs
)

###########################
# Set Script Version Info #
###########################
$CurrentScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CurrentScriptPath = $myInvocation.MyCommand.Definition
$CurrentScriptName = Split-Path -leaf $MyInvocation.MyCommand.Path
$CurrentDir = [System.IO.Directory]::GetCurrentDirectory()
$ScriptReleaseVersion = "0.1.7"
$ScriptInternalVersion = "01.12.03.19.13"
$ScriptLastUpdate = "3/19/2012"

############################
# Configure Script Options #
############################
Write-Output "Reading configured script options... `r "
Write-Verbose "Setting default options for script parameters...  `r "

Switch ($Verbose) {
    ## OPEN Switch Verbose
    $True { $VerbosePreference = "Continue" ; Write-Output "Script logging is set to verbose. `r " }
    $False { $VerbosePreference = "SilentlyContinue" ; Write-Output "Script logging is set to normal logging. `r " }
}  ## OPEN Switch Verbose

Switch ($Debug) {
    ## OPEN Switch Debug
    $True { $DebugPreference = "Continue" ; Write-Output "Script Debug logging is enabled. `r " }
    $False { $DebugPreference = "SilentlyContinue" ; }
}  ## OPEN Switch Debug

Write-Verbose "Check script parameters and based on setting configure proper script options & inform user...  `r "

###############################
# Set Environmental Variables #
###############################
# COMMON
write-output "Setting environmental variables... `r "
$DomainDNS = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name #Get AD Domain (lightweight & fast method)
$ADDomain = $DomainDNS
Write-Debug "Variable $DomainDNS & ADDomain is set to $DomainDNS  `r "
$TimeVal = get-date -uformat "%Y-%m-%d-%H-%M"
Write-Debug "Variable TimeVal is set to $TimeVal `r "
$LogDir = "C:\temp\Logs\"  #Standard location for script logs
Write-Debug "Variable LogDir is set to $LogDir  `r "
$DateTime = Get-Date #Get date/time
Write-Debug "Variable DateTime is set to $DateTime  `r "
$Separator = "#"  #Create separation line
$Sepline = $Separator * 75  #Create separation line
if (!(Test-Path $LogDir)) { new-item -type Directory -path $LogDir }

# Script Specific

# Script Logging
$LogFileName = "FindSPNs-$DomainDNS-$TimeVal.log"
$LogFile = $LogDir + $LogFileName
Write-Debug "Variable LogFile is set to $LogFile  `r "

$CSVReportFileName = "FindSPNs-$DomainDNS-$TimeVal.csv"
$CSVReportFile = $LogDir + $CSVReportFileName
Write-Debug "Variable CSVReportFile is set to $CSVReportFile  `r "

##################
# Start Logging  #
##################
# Log all configuration changes shown on the screen during run-time in a transcript file.  This
# inforamtion can be used for troubleshooting if necessary
Write-Verbose "Start Logging to $LogFile  `r "

Start-Transcript $LogFile -force

###############################
# Display Script Version Info #
###############################

Write-Output " `r "
Write-Output "============================================================  `r "
Write-Output "Script Name: $CurrentScriptName  `r "
Write-Output "Script Path: $CurrentScriptPath  `r "
Write-Output "Script Release Version: $ScriptReleaseVersion  `r "
Write-Output "Script Internal Version: $ScriptInternalVersion  `r "
Write-Output "Script Last Update: $ScriptLastUpdate  `r "
Write-Output "============================================================  `r "
Write-Output " `r "

## Process Start Time
$ProcessStartTime = Get-Date
Write-Verbose " `r "
write-Verbose "Script initialized by $CurrentUserName and started processing at $ProcessStartTime `r "
Write-Verbose " `r "

################################################
# Import Active Directory Powershell Elements  #
################################################
write-Verbose "Configuring Powershell environment... `r "
Write-Verbose "Importing Active Directory Powershell module `r "
import-module ActiveDirectory

#############################################
# Get Active Directory Forest & Domain Info #  20120201-15
#############################################
# Get Forest Info
write-output "Gathering Active Directory Forest Information..." `r
Write-Verbose "Running Get-ADForest Powershell command `r"
$ADForestInfo = Get-ADForest

$ADForestApplicationPartitions = $ADForestInfo.ApplicationPartitions
$ADForestCrossForestReferences = $ADForestInfo.CrossForestReferences
$ADForestDomainNamingMaster = $ADForestInfo.DomainNamingMaster
$ADForestDomains = $ADForestInfo.Domains
$ADForestForestMode = $ADForestInfo.ForestMode
$ADForestGlobalCatalogs = $ADForestInfo.GlobalCatalogs
$ADForestName = $ADForestInfo.Name
$ADForestPartitionsContainer = $ADForestInfo.PartitionsContainer
$ADForestRootDomain = $ADForestInfo.RootDomain
$ADForestSchemaMaster = $ADForestInfo.SchemaMaster
$ADForestSites = $ADForestInfo.Sites
$ADForestSPNSuffixes = $ADForestInfo.SPNSuffixes
$ADForestUPNSuffixes = $ADForestInfo.UPNSuffixes

# Get Domain Info
write-output "Gathering Active Directory Domain Information..." `r
Write-Verbose "Performing Get-ADDomain powershell command `r"
$ADDomainInfo = Get-ADDomain

$ADDomainAllowedDNSSuffixes = $ADDomainInfo.ADDomainAllowedDNSSuffixes
$ADDomainChildDomains = $ADDomainInfo.ChildDomains
$ADDomainComputersContainer = $ADDomainInfo.ComputersContainer
$ADDomainDeletedObjectsContainer = $ADDomainInfo.DeletedObjectsContainer
$ADDomainDistinguishedName = $ADDomainInfo.DistinguishedName
$ADDomainDNSRoot = $ADDomainInfo.DNSRoot
$ADDomainDomainControllersContainer = $ADDomainInfo.DomainControllersContainer
$ADDomainDomainMode = $ADDomainInfo.DomainMode
$ADDomainDomainSID = $ADDomainInfo.DomainSID
$ADDomainForeignSecurityPrincipalsContainer = $ADDomainInfo.ForeignSecurityPrincipalsContainer
$ADDomainForest = $ADDomainInfo.Forest
$ADDomainInfrastructureMaster = $ADDomainInfo.InfrastructureMaster
$ADDomainLastLogonReplicationInterval = $ADDomainInfo.LastLogonReplicationInterval
$ADDomainLinkedGroupPolicyObjects = $ADDomainInfo.LinkedGroupPolicyObjects
$ADDomainLostAndFoundContainer = $ADDomainInfo.LostAndFoundContainer
$ADDomainName = $ADDomainInfo.Name
$ADDomainNetBIOSName = $ADDomainInfo.NetBIOSName
$ADDomainObjectClass = $ADDomainInfo.ObjectClass
$ADDomainObjectGUID = $ADDomainInfo.ObjectGUID
$ADDomainParentDomain = $ADDomainInfo.ParentDomain
$ADDomainPDCEmulator = $ADDomainInfo.PDCEmulator
$ADDomainQuotasContainer = $ADDomainInfo.QuotasContainer
$ADDomainReadOnlyReplicaDirectoryServers = $ADDomainInfo.ReadOnlyReplicaDirectoryServers
$ADDomainReplicaDirectoryServers = $ADDomainInfo.ReplicaDirectoryServers
$ADDomainRIDMaster = $ADDomainInfo.RIDMaster
$ADDomainSubordinateReferences = $ADDomainInfo.SubordinateReferences
$ADDomainSystemsContainer = $ADDomainInfo.SystemsContainer
$ADDomainUsersContainer = $ADDomainInfo.UsersContainer
$DomainDNS = $ADDomainDNSRoot

######################################
# Discover Local Global Catalog (DC) #
######################################

if ($TargetGC) {
    ## OPEN if TargetGC has a value
    $GCInfo = Get-ADDomainController $TargetGC
    if ($GCInfo.OperatingSystemVersion -lt 6.0) {
        ## OPEN if TargetGC is not running Windows 2008 or higher
        $LocalSite = (Get-ADDomainController -Discover).Site
        $NewTargetGC = Get-ADDomainController -Discover -Service 6 -SiteName $LocalSite
        if (!$NewTargetGC)
        { $NewTargetGC = Get-ADDomainController -Discover -Service 6 -NextClosestSite }
        $LocalGC = $NewTargetGC.HostName + ":3268"
    } ## CLOSE if TargetGC is not running Windows 2008 or higher

    else { $LocalGC = $GCInfo.HostName + ":3268" }
} ## CLOSE if TargetGC has a value

else {
    ## OPEN else TargetGC is not set
    Write-Output "Discover Local GC running ADWS `r "
    $LocalSite = (Get-ADDomainController -Discover).Site
    $NewTargetGC = Get-ADDomainController -Discover -Service 6 -SiteName $LocalSite
    if (!$NewTargetGC)
    { $NewTargetGC = Get-ADDomainController -Discover -Service 6 -NextClosestSite }
    $LocalGC = $NewTargetGC.HostName + ":3268"
} ## CLOSE else TargetGC is not set

if ($UPNName) {
    ## OPEN if UPNName was provided
    ####################################
    # Find Objects with a Specific UPN #
    ####################################

    Write-Output "Identify User Objects configured with the UPN: $UPNName `r "
    $Time = (Measure-Command `
        { [array]$UPNObjectList = Get-ADObject -Server "$LocalGC" -filter { (ObjectClass -eq "User") -OR (ObjectClass -eq "Computer") } `
                -property name, distinguishedname, UserPrincipalName | Where-Object { $_.UserPrincipalName -like "$UPNName" }
        }).Seconds
    [int]$UPNObjectListCount = $UPNObjectList.Count
    Write-Output "Discovered $UPNObjectListCount User objects configured with the UPN: $UPNName in $Time Seconds `r "
    Write-Output "The following $UPNObjectListCount user objects are configured with the UPN: `r "
    $UPNObjectList

}  ## CLOSE if UPNName was provided


if ($SPNName) {
    ## OPEN if SPNName was provided
    ####################################
    # Find Objects with a Specific SPN #
    ####################################

    Write-Output "Identify User and Computer Objects configured with the Service Principal Name: $SPNName `r "
    $Time = (Measure-Command `
        { [array]$SPNObjectList = Get-ADObject -Server "$LocalGC" -filter { (ObjectClass -eq "User") -OR (ObjectClass -eq "Computer") } `
                -property name, distinguishedname, ServicePrincipalName | Where-Object { $_.ServicePrincipalName -like "$SPNName" }
        }).Seconds
    [int]$SPNObjectListCount = $SPNObjectList.Count

    Write-Output "Discovered $SPNObjectListCount User objects configured with the SPN: $SPNName in $Time Seconds `r "
    Write-Output "The following $SPNObjectListCount user objects are configured with the SPN: `r "
    $SPNObjectList

}  ## CLOSE if SPNName was provided


if ($FindDuplicateUPNs -eq $True) {
    ## OPEN if FindDuplicateUPNs = True
    ###########################
    # Discover Duplicate UPNs #
    ###########################
    if ($AllUPNList) { Clear-Variable AllUPNList ; Clear-Variable DuplicateUPNList }

    Write-Output "Identify User Objects with configured User Principal Names `r "
    $Time = (Measure-Command `
        { $UPNObjectList = Get-ADObject -Server "$LocalGC" -filter { (ObjectClass -eq "User") -OR (ObjectClass -eq "Computer") } `
                -property name, distinguishedname, UserPrincipalName | Where-Object { $_.UserPrincipalName -ne $NULL }
        }).Seconds
    $UPNObjectListCount = $UPNObjectList.Count
    Write-Output "Discovered $UPNObjectListCount User with UPNs in $Time Seconds `r "

    Write-Output "Build a list of all UPNs `r "
    $Time = (Measure-Command `
        { foreach ($UPN in $UPNObjectList) {
                ## OPEN ForEach Item in ObjectList
                ForEach ($Object in $UPN.ServicePrincipalName) {
                    ## OPEN ForEach Object in Item.ServicePrincipalName
                    [array]$AllUPNList += $Object
                }  ## CLOSE ForEach Object in Item.ServicePrincipalName
            }  ## CLOSE ForEach Item in ObjectList
        }).Seconds
    Write-Output "UPN List created in $Time Seconds `r "

    Write-Output "Find duplicates in the UPN list `r "
    $Time = (Measure-Command `
        {
            [array]$AllUPNList = $AllUPNList | Sort-Object
            [array]$UniqueUPNs = $AllUPNList | Select-Object -Unique
            [array]$DuplicateUPNs = Compare-Object -ReferenceObject $UniqueUPNs -DifferenceObject $AllUPNList
        }).Seconds
    [int]$UniqueUPNSCount = $UniqueUPNs.Count
    ForEach ($DupUPN in $DuplicateUPNs) {
        ## OPEN ForEach Dup in DuplicateUPNs
        [array]$DuplicateUPNList += $DupUPN.InputObject
    }  ## CLOSE ForEach Dup in DuplicateUPNs
    [int]$DuplicateUPNsCount = $DuplicateUPNList.Count
    Write-Output "Discovered $UniqueUPNsCount Unique UPNs in $Time Seconds `r "
    Write-Output "Discovered $DuplicateUPNsCount Duplicate UPNs in $Time Seconds `r "
    Write-Output " `r "

    Write-Output "Identifying objects containing the duplicate UPNs... `r "
    ForEach ($UPN in $DuplicateUPNList) {
        ## OPEN ForEach UPN in DuplicateUPNs
        $DupUPNObjects = $UPNObjectList | Where-Object { $_.ServicePrincipalName -eq $UPN }
        Write-Output " `r "
        Write-Output "The UPN $UPN is configured on the following objects:  `r "

        ForEach ($Obj in $DupUPNObjects) {
            ## OPEN ForEach Obj in DupUPNObjects
            [string]$UPNObjectUPN = $UPN  # $Obj.ServicePrincipalName
            $UPNObjectName = $Obj.Name
            $UPNObjectClass = $Obj.ObjectClass
            $UPNObjectDN = $Obj.DistinguishedName

            Write-Output "     *  $UPNObjectName ($UPNObjectClass) has the associated UPN: $UPN [$UPNObjectDN] `r "

            Write-Verbose "Creating Inventory Object for $Obj..."
            $InventoryObject = New-Object -TypeName PSObject
            $InventoryObject | Add-Member -MemberType NoteProperty -Name UPN -Value ($UPN)
            $InventoryObject | Add-Member -MemberType NoteProperty -Name ObjectName -Value $UPNObjectName
            $InventoryObject | Add-Member -MemberType NoteProperty -Name UPNObjectClass -Value $UPNObjectClass
            $InventoryObject | Add-Member -MemberType NoteProperty -Name ObjectDN -Value $UPNObjectDN
            [array]$AllInventory += $InventoryObject

        }  ## CLOSE ForEach Obj in DupUPNObjects
    }  ## CLOSE ForEach UPN in DuplicateUPNs

    # Create Inventory Object
    [int]$AllInventoryCount = $AllInventory.Count
    Write-Output "Exporting File Information ($AllInventoryCount records) to CSV Report file ($CSVReportFile)..."
    $AllInventory | Export-CSV $CSVReportFile -NoType

}  ## CLOSE if FindDuplicateUPNs = True


if ($FindDuplicateSPNs -eq $True) {
    ## OPEN if FindDuplicateSPNs = True
    ###########################
    # Discover Duplicate SPNs #
    ###########################
    if ($AllSPNList) { Clear-Variable AllSPNList ; Clear-Variable DuplicateSPNList }

    Write-Output "Identify User and Computer Objects with configured Service Principal Names `r "
    $Time = (Measure-Command `
        { $ObjectList = Get-ADObject -Server "$LocalGC" -filter { (ObjectClass -eq "User") -OR (ObjectClass -eq "Computer") } `
                -property name, distinguishedname, ServicePrincipalName | Where-Object { $_.ServicePrincipalName -ne $NULL }
        }).Seconds
    $ObjectListCount = $ObjectList.Count
    Write-Output "Discovered $ObjectListCount User and Computer Objects with SPNs in $Time Seconds `r "

    Write-Output "Build a list of all SPNs `r "
    $Time = (Measure-Command `
        { ForEach ($Item in $ObjectList) {
                ## OPEN ForEach Item in ObjectList
                ForEach ($Object in $Item.ServicePrincipalName) {
                    ## OPEN ForEach Object in Item.ServicePrincipalName
                    [array]$AllSPNList += $Object
                }  ## CLOSE ForEach Object in Item.ServicePrincipalName
            }  ## CLOSE ForEach Item in ObjectList
        }).Seconds
    Write-Output "SPN List created in $Time Seconds `r "

    Write-Output "Find duplicates in the SPN list `r "
    $Time = (Measure-Command `
        {
            [array]$AllSPNList = $AllSPNList | Sort-Object
            [array]$UniqueSPNs = $AllSPNList | Select-Object -Unique
            [array]$DuplicateSPNs = Compare-Object -ReferenceObject $UniqueSPNs -DifferenceObject $AllSPNList
        }).Seconds
    [int]$UniqueSPNSCount = $UniqueSPNS.Count
    ForEach ($Dup in $DuplicateSPNs) {
        ## OPEN ForEach Dup in DuplicateSPNs
        [array]$DuplicateSPNList += $Dup.InputObject
    }  ## CLOSE ForEach Dup in DuplicateSPNs
    [int]$DuplicateSPNsCount = $DuplicateSPNList.Count
    Write-Output "Discovered $UniqueSPNSCount Unique SPNs in $Time Seconds `r "
    Write-Output "Discovered $DuplicateSPNsCount Duplicate SPNs in $Time Seconds `r "
    Write-Output " `r "

    Write-Output "Identifying objects containing the duplicate SPNs... `r "
    ForEach ($SPN in $DuplicateSPNList) {
        ## OPEN ForEach SPN in DuplicateSPNs
        $DupSPNObjects = $ObjectList | Where-Object { $_.ServicePrincipalName -eq $SPN }
        Write-Output " `r "
        Write-Output "The SPN $SPN is configured on the following objects:  `r "

        ForEach ($Obj in $DupSPNObjects) {
            ## OPEN ForEach Obj in DupSPNObjects
            [string]$SPNObjectSPN = $SPN  # $Obj.ServicePrincipalName
            $SPNObjectName = $Obj.Name
            $SPNObjectClass = $Obj.ObjectClass
            $SPNObjectDN = $Obj.DistinguishedName

            Write-Output "     *  $SPNObjectName ($SPNObjectClass) has the associated SPN: $SPN [$SPNObjectDN] `r "

            Write-Verbose "Creating Inventory Object for $Obj..."
            $InventoryObject = New-Object -TypeName PSObject
            $InventoryObject | Add-Member -MemberType NoteProperty -Name SPN -Value ($SPN)
            $InventoryObject | Add-Member -MemberType NoteProperty -Name ObjectName -Value $SPNObjectName
            $InventoryObject | Add-Member -MemberType NoteProperty -Name SPNObjectClass -Value $SPNObjectClass
            $InventoryObject | Add-Member -MemberType NoteProperty -Name ObjectDN -Value $SPNObjectDN
            [array]$AllInventory += $InventoryObject

        }  ## CLOSE ForEach Obj in DupSPNObjects
    }  ## CLOSE ForEach SPN in DuplicateSPNs

    # Create Inventory Object
    [int]$AllInventoryCount = $AllInventory.Count
    Write-Output "Exporting File Information ($AllInventoryCount records) to CSV Report file ($CSVReportFile)..."
    $AllInventory | Export-CSV $CSVReportFile -NoType
}  ## CLOSE if FindDuplicateSPNs = True

########################################
# Provide Script Processing Statistics #
########################################

$ProcessEndTime = Get-Date
Write-output "Script started processing at $ProcessStartTime and completed at $ProcessEndTime." `r
$TotalProcessTimeCalc = $ProcessEndTime - $ProcessStartTime
$TotalProcessTime = "{0:HH:mm}" -f $TotalProcessTimeCalc
Write-output "" `r
Write-output "The script completed processing in $TotalProcessTime." `r

#################
# Stop Logging  #
#################

#Stop logging the configuration changes in a transript file
Stop-Transcript

Write-output "Review the logfile $LogFile for script operation information." `r

Back to Top


Download

Please feel free to copy parts of the script or if you would like to download the entire script, simple click the download button. You can download the complete repository in a zip file by clicking the Download link in the menu bar on the left hand side of the page.


Report Issues

You can report an issue or contribute to this site on GitHub. Simply click the button below and add any relevant notes. I will attempt to respond to all issues as soon as possible.

Issue


Back to Top