scripts/pending/Get-SQLServerLinkCrawl.ps1

Function Get-SQLCrawl{
    <#
    .SYNOPSIS
    Get-SQLCrawl attempts to enumerate and follow MSSQL database links.
    .DESCRIPTION
    Get-SQLCrawl attempts to enumerate and follow MSSQL database links. The function enumerates database names, versions, and links,
    and then enumerates the MSSQL user and the privileges that the link path has.
    .EXAMPLE
    Get-SQLCrawl -Instance "servername\instancename" -ByLinkPath
    .PARAMETER Username
    SQL Server or domain account to authenticate with.
    .PARAMETER Password
    SQL Server or domain account password to authenticate with.
    .PARAMETER Credential
    Windows credentials.
    .PARAMETER Instance
    SQL Server instance to connection to.
    .PARAMETER DAC
    Dedicated Administrator Connection (DAC).
    .PARAMETER TimeOut
    Connection timeout.
    .PARAMETER Query
    Custom SQL query to run on each server.
    .PARAMETER Export
    Convert collected data to exportable format.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,
        HelpMessage="SQL Server or domain account to authenticate with.")]
        [string]$Username,

        [Parameter(Mandatory=$false,
        HelpMessage="SQL Server or domain account password to authenticate with.")]
        [string]$Password,

        [Parameter(Mandatory=$false,
        HelpMessage="Windows credentials.")]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,
        
        [Parameter(Mandatory=$false,
        ValueFromPipelineByPropertyName=$true,
        HelpMessage="SQL Server instance to connection to.")]
        [string]$Instance,

        [Parameter(Mandatory=$false,
        HelpMessage="Dedicated Administrator Connection (DAC).")]
        [Switch]$DAC,

        [Parameter(Mandatory=$false,
        HelpMessage="Connection timeout.")]
        [int]$TimeOut = 2,

        [Parameter(Mandatory=$false,
        HelpMessage="Custom SQL query to run on each server.")]
        [string]$Query,

        [Parameter(Mandatory=$false,
        HelpMessage="Convert collected data to exportable format.")]
        [switch]$Export
    )

    Begin
    {   
        $List = @()

        $Server = New-Object PSObject -Property @{ Name=""; Version=""; Links=@(); Path=@(); User=""; Sysadmin=""; CustomQuery=""}

        $List += $Server
        $SqlInfoTable = New-Object System.Data.DataTable
    }
    
    Process
    {
        $i=1
        while($i){
            $i--
            foreach($Server in $List){
                if($Server.Name -eq "") {
                    $List = (Get-SQLServerLink -list $List -server $Server -query $Query)
                    $i++

                    # Verbose output
                    $myname = $server.name
                    $myLinkPath = $server.path                   
                    $myPath = $myLinkPath -join ' -> '                 
                    $mylinks = $server.links
                    $mysysadmin = $server.sysadmin
                    $myuser = $server.user
                    $myLinkCount = $mylinks.count
                 
                    write-verbose "--------------------------------"
                    Write-Verbose " Server: $myname"
                    write-verbose "--------------------------------"
                    write-verbose " - Link Path to server: $myPath"                    
                    write-verbose " - Link Login: $myuser"                                   
                    write-verbose " - Link IsSysAdmin: $mysysadmin"
                    write-verbose " - Link Count: $myLinkCount"                    
                    write-verbose " - Links on this server:$mylinks"
                }   
            } 
        }

        if($Export){
            $LinkList = New-Object System.Data.Datatable
            [void]$LinkList.Columns.Add("Name")
            [void]$LinkList.Columns.Add("Version")
            [void]$LinkList.Columns.Add("Path")
            [void]$LinkList.Columns.Add("Links")
            [void]$LinkList.Columns.Add("User")
            [void]$LinkList.Columns.Add("Sysadmin")
            [void]$LinkList.Columns.Add("CustomQuery")

            foreach($Server in $List){
                [void]$LinkList.Rows.Add($Server.name,$Server.version,$Server.path -join " -> ", $Server.links -join ",", $Server.user, $Server.Sysadmin, $Server.CustomQuery -join ",")
            }

            return $LinkList
        } else {
            return $List
        }
    }
  
    End
    {
    }
}

Function Get-SQLServerLink{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,
        HelpMessage="List of server objects identified during the crawling")]
        $List,
        
        [Parameter(Mandatory=$true,
        HelpMessage="Server object to be tested")]
        $Server,

        [Parameter(Mandatory=$false,
        HelpMessage="Custom SQL query to run")]
        $Query
    )

    Begin
    {
        $SqlInfoQuery = "select @@servername as servername, @@version as version, system_user as linkuser, is_srvrolemember('sysadmin') as role"
        $SqlLinksQuery = "select srvname from master..sysservers where dataaccess=1"
    }

    Process
    {
        $SqlInfoTable = Get-SqlQuery -instance $Instance -Query ((Get-SQLLinkQuery -path $Server.Path -sql $SqlInfoQuery)) -Timeout $Timeout -Username $UserName -Password $Password -Credential $Credential
        if($SqlInfoTable.Servername -ne $null){
            $Server.Name = $SqlInfoTable.Servername
            $Server.Version = [System.String]::Join("",(($SqlInfoTable.Version)[10..25]))
            $Server.Sysadmin = $sqlInfoTable.role
            $Server.User = $sqlInfoTable.linkuser
            
            if($List.Count -eq 1) { $Server.Path += ,$sqlInfoTable.servername }

            $SqlInfoTable = Get-SqlQuery -instance $Instance -Query ((Get-SQLLinkQuery -path $Server.Path -sql $SqlLinksQuery)) -Timeout $Timeout -Username $UserName -Password $Password -Credential $Credential
            $Server.Links = [array]$SqlInfoTable.srvname

            if($Query -ne ""){
                if($Query -like '*xp_cmdshell*'){
                    $Query =  $Query + " WITH RESULT SETS ((output VARCHAR(8000)))"
                }
                if($Query -like '*xp_dirtree*'){
                    $Query = $Query + " WITH RESULT SETS ((output VARCHAR(8000), depth int))"
                }
                $SqlInfoTable = Get-SqlQuery -instance $Instance -Query ((Get-SQLLinkQuery -path $Server.Path -sql $Query)) -Timeout $Timeout -Username $UserName -Password $Password -Credential $Credential
                if($Query -like '*WITH RESULT SETS*'){
                    $Server.CustomQuery = $SqlInfoTable.output
                } else {
                    $Server.CustomQuery = $SqlInfoTable
                }
            }

            if(($Server.Path | Sort-Object | Get-Unique).Count -eq ($Server.Path).Count){
                foreach($Link in $Server.Links){
                    $Linkpath = $Server.Path + $Link
                    $List += ,(New-Object PSObject -Property @{ Name=""; Version=""; Links=@(); Path=$Linkpath; User=""; Sysadmin=""; CustomQuery="" })
                }
            }
        } else {
            $Server.Name = "Broken Link"
        }
        return $List
    }
}

Function Get-SQLLinkQuery{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,
        HelpMessage="SQL link path to crawl")]
        $Path=@(),
        
        [Parameter(Mandatory=$false,
        HelpMessage="SQL query to build the crawl path around")]
        $Sql, 
        
        [Parameter(Mandatory=$false,
        HelpMessage="Counter to determine how many single quotes needed")]
        $Ticks=0

    )
    if ($Path.length -le 1){
        return($Sql -replace "'", ("'"*[Math]::pow(2,$Ticks)))
    } else {
        return("select * from openquery(`""+$Path[1]+"`","+"'"*[Math]::pow(2,$Ticks)+
        (Get-SQLLinkQuery -path $Path[1..($Path.Length-1)] -sql $Sql -ticks ($Ticks+1))+"'"*[Math]::pow(2,$Ticks)+")")
    }
}

Function  Get-SQLQuery
{
    <#
            .SYNOPSIS
            Executes a query on target SQL servers.This
            .PARAMETER Username
            SQL Server or domain account to authenticate with.
            .PARAMETER Password
            SQL Server or domain account password to authenticate with.
            .PARAMETER Credential
            SQL Server credential.
            .PARAMETER Instance
            SQL Server instance to connection to.
            .PARAMETER DAC
            Connect using Dedicated Admin Connection.
            .PARAMETER Database
            Default database to connect to.
            .PARAMETER TimeOut
            Connection time out.
            .PARAMETER SuppressVerbose
            Suppress verbose errors. Used when function is wrapped.
            .PARAMETER Threads
            Number of concurrent threads.
            .PARAMETER Query
            Query to be executed on the SQL Server.
            .EXAMPLE
            PS C:\> Get-SQLQuery -Verbose -Instance "SQLSERVER1.domain.com\SQLExpress" -Query "Select @@version" -Threads 15
            .EXAMPLE
            PS C:\> Get-SQLQuery -Verbose -Instance "SQLSERVER1.domain.com,1433" -Query "Select @@version" -Threads 15
            .EXAMPLE
            PS C:\> Get-SQLInstanceDomain | Get-SQLQuery -Verbose -Query "Select @@version" -Threads 15
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false,
                ValueFromPipelineByPropertyName = $true,
        HelpMessage = 'SQL Server or domain account to authenticate with.')]
        [string]$Username,

        [Parameter(Mandatory = $false,
                ValueFromPipelineByPropertyName = $true,
        HelpMessage = 'SQL Server or domain account password to authenticate with.')]
        [string]$Password,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Windows credentials.')]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false,
                ValueFromPipelineByPropertyName = $true,
        HelpMessage = 'SQL Server instance to connection to.')]
        [string]$Instance,

        [Parameter(Mandatory = $false,
                ValueFromPipelineByPropertyName = $true,
        HelpMessage = 'SQL Server query.')]
        [string]$Query,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Connect using Dedicated Admin Connection.')]
        [Switch]$DAC,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Default database to connect to.')]
        [String]$Database,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Connection timeout.')]
        [int]$TimeOut,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Suppress verbose errors. Used when function is wrapped.')]
        [switch]$SuppressVerbose,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Return error message if exists.')]
        [switch]$ReturnError
    )

    Begin
    {
        # Setup up data tables for output
        $TblQueryResults = New-Object -TypeName System.Data.DataTable
    }

    Process
    {
        # Setup DAC string
        if($DAC)
        {
            # Create connection object
            $Connection = Get-SQLConnectionObject -Instance $Instance -Username $Username -Password $Password -Credential $Credential -TimeOut $TimeOut -DAC -Database $Database
        }
        else
        {
            # Create connection object
            $Connection = Get-SQLConnectionObject -Instance $Instance -Username $Username -Password $Password -Credential $Credential -TimeOut $TimeOut -Database $Database
        }

        # Parse SQL Server instance name
        $ConnectionString = $Connection.Connectionstring
        $Instance = $ConnectionString.split(';')[0].split('=')[1]

        # Check for query
        if($Query)
        {
            # Attempt connection
            try
            {
                # Open connection
                $Connection.Open()

                if(-not $SuppressVerbose)
                {
                    #Write-Verbose -Message "$Instance : Connection Success."
                }

                # Setup SQL query
                $Command = New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList ($Query, $Connection)

                # Grab results
                $Results = $Command.ExecuteReader()

                # Load results into data table
                $TblQueryResults.Load($Results)

                # Close connection
                $Connection.Close()

                # Dispose connection
                $Connection.Dispose()
            }
            catch
            {
                # Connection failed - for detail error use Get-SQLConnectionTest
                if(-not $SuppressVerbose)
                {
                    #Write-Verbose -Message "$Instance : Connection Failed."
                }

                if($ReturnError)
                {
                    $ErrorMessage = $_.Exception.Message
                    #Write-Verbose " Error: $ErrorMessage"
                }
            }
        }
        else
        {
            Write-Output -InputObject 'No query provided to Get-SQLQuery function.'
            Break
        }
    }

    End
    {
        # Return Results
        if($ReturnError)
        {
            $ErrorMessage
        }
        else
        {
            $TblQueryResults
        }
    }
}

Function  Get-SQLConnectionObject
{
    <#
            .SYNOPSIS
            Creates a object for connecting to SQL Server.
            .PARAMETER Username
            SQL Server or domain account to authenticate with.
            .PARAMETER Password
            SQL Server or domain account password to authenticate with.
            .PARAMETER Credential
            SQL Server credential.
            .PARAMETER Database
            Default database to connect to.
            .EXAMPLE
            PS C:\> Get-SQLConnectionObject -Username MySQLUser -Password MySQLPassword
            StatisticsEnabled : False
            AccessToken :
            ConnectionString : Server=SQLServer1;Database=Master;User ID=MySQLUser;Password=MySQLPassword;Connection Timeout=1
            ConnectionTimeout : 1
            Database : Master
            DataSource : SQLServer1
            PacketSize : 8000
            ClientConnectionId : 00000000-0000-0000-0000-000000000000
            ServerVersion :
            State : Closed
            WorkstationId : SQLServer1
            Credential :
            FireInfoMessageEventOnUserErrors : False
            Site :
            Container :
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false,
        HelpMessage = 'SQL Server or domain account to authenticate with.')]
        [string]$Username,

        [Parameter(Mandatory = $false,
        HelpMessage = 'SQL Server or domain account password to authenticate with.')]
        [string]$Password,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Windows credentials.')]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false,
                ValueFromPipelineByPropertyName = $true,
        HelpMessage = 'SQL Server instance to connection to.')]
        [string]$Instance,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Dedicated Administrator Connection (DAC).')]
        [Switch]$DAC,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Default database to connect to.')]
        [String]$Database,

        [Parameter(Mandatory = $false,
        HelpMessage = 'Connection timeout.')]
        [string]$TimeOut = 1
    )

    Begin
    {
        # Setup DAC string
        if($DAC)
        {
            $DacConn = 'ADMIN:'
        }
        else
        {
            $DacConn = ''
        }

        # Set database filter
        if(-not $Database)
        {
            $Database = 'Master'
        }
    }

    Process
    {
        # Check for instance
        if ( -not $Instance)
        {
            $Instance = $env:COMPUTERNAME
        }

        # Create connection object
        $Connection = New-Object -TypeName System.Data.SqlClient.SqlConnection

        # Check for username and password
        if($Username -and $Password)
        {
            # Setup connection string with SQL Server credentials
            $Connection.ConnectionString = "Server=$DacConn$Instance;Database=$Database;User ID=$Username;Password=$Password;Connection Timeout=$TimeOut"
        }
        else
        {
            # Get connecting user
            $UserDomain = [Environment]::UserDomainName
            $Username = [Environment]::UserName
            $ConnectionectUser = "$UserDomain\$Username"

            # Status user
            Write-Debug -Message "Attempting to authenticate to $DacConn$Instance as current Windows user ($ConnectionectUser)..."

            # Setup connection string with trusted connection
            $Connection.ConnectionString = "Server=$DacConn$Instance;Database=$Database;Integrated Security=SSPI;Connection Timeout=1"

            <#
                    # Check for provided credential
                    if ($Credential){
                    $Username = $credential.Username
                    $Password = $Credential.GetNetworkCredential().Password
                    # Setup connection string with SQL Server credentials
                    $Connection.ConnectionString = "Server=$DacConn$Instance;Database=$Database;User ID=$Username;Password=$Password;Connection Timeout=$TimeOut"
                    }
            #>

        }

        # Return the connection object
        return $Connection
    }

    End
    {
    }
}


# Example commands
#Get-SQLCrawl -instance "SQLSERVER1\Instance1" -Query "select name from master..sysdatabases"
#Get-SQLCrawl -instance "SQLSERVER1\Instance1" -Query "select name from master..sysdatabases" | select name,version,path,links,user,sysadmin,customquery | format-table
#Get-SQLCrawl -instance "SQLSERVER1\Instance1" -Query "select name from master..sysdatabases" | where name -ne "Broken Link" | select name,version,path,links,user,sysadmin,customquery | format-table
#Get-SQLCrawl -instance "SQLSERVER1\Instance1" -Query "exec master..xp_cmdshell 'whoami'" | format-table
#Get-SQLCrawl -instance "SQLSERVER1\Instance1" -Query "exec xp_dirtree 'c:\temp'" -Export | format-table
#Get-SQLCrawl -instance "SQLSERVER1\Instance1" -Query "select name from master..sysdatabases" -Export | where name -ne "broken link" | sort name | Format-Table