Array of psobjects missing members in scanning TCP and UDP ports script

130 views Asked by At

I'm running this script which scans ports, outputs the result to a psobject. It first checks if the port is TCP or UDP, then it runs a switch depending on whether the port is 3389, 443 or something else. If it's 3389 or 443, it uses the get-portcertificate function to get the certificate's subject and add this to the members of the object.

The problem is, when the script runs, I can see from "$obj | ft" line that it has added the RDP Cert and SSL Cert members to the object, but at the "$objServersList | ft" line these two are missing. Is it something to do with the fact that some instances of $obj have the "SSL Cert" member but some only have "RDP Cert"? How do I make it 'merge' those two types of object (or just put a null value if not appropriate)

enter image description here

I'm using Caleb Keene's get-portcertificate function, my code is below the function.


Function Get-PortCertificate {

<#

.SYNOPSIS
    Returns certificate information from a listening TLS/SSL service port.

.DESCRIPTION
    Gets the associated certificate from a TLS/SSL application service port.

.PARAMETER  Computername
    Hostname or IP address of the target system (Default: localhost).  The function uses the supplied computername to validate with the certificate's subject name(s).

.PARAMETER  Port
    Port to retrieve SSL certificate (Default: 443).

.PARAMETER  Path
    Directory path to save SSL certificate(s).  

.PARAMETER  DownloadChain
    Save all chain certificates to file.  A certificate chain folder will be created under the specfied -path directory.  -DownloadChain is dependent on the path parameter.

.NOTES
    Name: Get-PortCertificate
    Author: Caleb Keene
    Updated: 08-30-2016
    Version: 1.2

.EXAMPLE
    Get-PortCertificate -Computername Server1 -Port 3389 -Path C:\temp -verbose

.EXAMPLE
    "server1","server2","server3" | Get-PortCertificate 
#>   
 

[CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)]
        [Alias('IPAddress','Server','Computer')]              
        [string]$ComputerName =  $env:COMPUTERNAME,  
        [Parameter(Mandatory = $false,Position = 1)]
        [ValidateRange(1,65535)]
        [int]$Port = 443,
        [Parameter(Mandatory = $false)]
        [ValidateNotNullorEmpty()]
        [string]$Path

    )
    #use a dynamic parameter to prevent -downloadchain without -path.
    DynamicParam {
        #Need some sort of conditional check before allowing Dynamic Parameter
        If ($PSBoundParameters.ContainsKey('Path')) {
            #Same as [Parameter()]
            $attribute = new-object System.Management.Automation.ParameterAttribute
            $attribute.Mandatory = $false
            $AttributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($attribute)

            #Build out the Dynamic Parameter
            # Need the Parameter Name, Type and Attribute Collection (Built already)
            $DynamicParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter("DownloadChain", [switch], $AttributeCollection)
            
            $ParamDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamDictionary.Add("DownloadChain", $DynamicParam)
            return $ParamDictionary
        }        
    }    

    Begin{
        #make sure the version is supported
        if ($psversiontable.psversion.Major -le 2 ){
                Write-warning "Function requires PowerShell version 3 or later."
                break            
        }

        #add a custom type name to control our objects default display properties
        try{ Update-TypeData -TypeName 'Get.PortCertificate' -DefaultDisplayPropertySet Subject,Issuer,NotAfter,NotBefore,ExpiresIn,CertificateValidNames,TargetName,TargetNameStatus,TargetNameStatusDetails,TargetNameIsValid,ChainPath,ChainStatus,ChainStatusDetails,CertificateIsValid -ErrorAction stop}
        catch{}

        #validate that the path is a filesystem directory
        if ($path) { 

            if(-not(test-path -PathType Container FileSystem::$path)){
                Write-warning "The supplied directory path is not valid: $path"
                break
            }

        }
       
    }

    Process {
        
        #make sure we are able to establish a port connection

        #Set our connection timeout
        $timeout = 1000

        #Create object to test the port connection
        $tcpobject = New-Object System.Net.Sockets.TcpClient 

        #Connect to remote port               
        $connect = $tcpobject.BeginConnect($ComputerName,$Port,$null,$null) 

        #Configure connection timeout
        $wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false) 
        If (-NOT $Wait) {
            Write-Warning "[$($ComputerName)] Connection to port $($Port) timed out after $($timeout) milliseconds"
            return
        } Else {
            Try {
                [void]$tcpobject.EndConnect($connect)
                Write-Verbose "[$($ComputerName)] Successfully connected to port $($Port). Good!"
            } Catch {
                Write-Warning "[$($ComputerName)] $_"
                return
            }
        } 

        #Note: This also works for validating the port connection, but the default timeout when unable to connect is a bit long.
        <#
        try {
            (New-Object system.net.sockets.tcpclient -ArgumentList $computername,$port -ErrorAction stop).Connected
        }
        catch{            
            Write-Warning ("Unable to connect to {0} on port {1}"-f$ComputerName,$Port)
            return
        }
        #>


        Write-Verbose "[$($ComputerName)] Getting SSL certificate from port $($Port)."

        #create our webrequest object for the ssl connection
        $sslrequest = [Net.WebRequest]::Create("https://$ComputerName`:$port")
        $sslrequest.Timeout = 100000

        #make the connection and store the response (if any).
        try{$Response = $sslrequest.GetResponse()}
        catch{}

        #load the returned SSL certificate using x509certificate2 class
        if ($certificate = [Security.Cryptography.X509Certificates.X509Certificate2]$sslrequest.ServicePoint.Certificate.Handle){
            
            Write-Verbose "[$($ComputerName)] Certificate found!  Building certificate chain information and object data."

            #build our certificate chain object
            $chain = [Security.Cryptography.X509Certificates.X509Chain]::create()
            $isValid = $chain.Build($certificate)
            
            #get certificate subject names from our certificate extensions
            $validnames = @()
            try{[array]$validnames += @(($certificate.Extensions | ? {$_.Oid.Value -eq "2.5.29.17"}).Format($true).split("`n") | ? {$_} | % {$_.split("=")[1].trim()})}catch{}
            try{[array]$validnames += @($certificate.subject.split(",")[0].split("=")[1].trim())}catch{}
            
            #validate the target name
            for($i=0;$i -le $validnames.count - 1;$i++){
                if ($validnames[$i] -match '^\*'){
                    $wildcard = $validnames[$i] -replace '^\*\.'
                    if($computername -match "$wildcard$"){
                        $TargetNameIsValid = $true
                        break
                    }
                    $TargetNameIsValid = $false
                }
                else{
                    if($validnames[$i] -match "^$ComputerName$"){
                        $TargetNameIsValid = $true
                        break
                    }
                    $TargetNameIsValid = $false
                }
            }

            #create custom object to later convert to PSobject (required in order to use the custom type name's default display properties)
            $customized = $certificate | select *,
                @{n="ExtensionData";e={$_.Extensions | % {@{$_.oid.friendlyname.trim()=$_.format($true).trim()}}}},
                @{n="ResponseUri";e={if ($Response.ResponseUri){$Response.ResponseUri}else{$false}}},
                @{n="ExpiresIn";e={if((get-date) -gt $_.NotAfter){"Certificate has expired!"}else{$timespan = New-TimeSpan -end $_.notafter;"{0} Days - {1} Hours - {2} Minutes" -f $timespan.days,$timespan.hours,$timespan.minutes}}},
                @{n="TargetName";e={$ComputerName}},
                @{n="CertificateValidNames";e={$validnames}},
                @{n="ChainPath";e={$count=0;$chaincerts = @($chain.ChainElements.certificate.subject);$($chaincerts[($chaincerts.length -1) .. 0] | % {"{0,$(5+$count)}{1}" -f "---",$_;$count+=3}) -join "`n"}},
                @{n="ChainCertificates";e={@{"Certificates"=$chain.ChainElements.certificate}}},
                @{n="ChainStatus";e={if($isvalid -and !$_.chainstatus){"Good"}else{$chain.chainstatus.Status}}},
                @{n="ChainStatusDetails";e={if($isvalid -and !$_.chainstatus){"The certificate chain is valid."}else{$chain.chainstatus.StatusInformation.trim()}}},
                @{n="CertificateIsValid";e={$isValid}},
                @{n="TargetNameIsValid";e={$TargetNameIsValid}},
                @{n="TargetNameStatus";e={if($TargetNameIsValid){"Good"}else{"Invalid"}}},
                @{n="TargetNameStatusDetails";e={if($TargetNameIsValid){"The target name appears to be valid: $computername"}else{"TargetName $computername does not match any certificate subject name."}}} 
            
                 
            #get object properties for our PSObject
            $objecthash = [Ordered]@{}
            ($customized | Get-Member -MemberType Properties).name | % {$objecthash+=@{$_=$customized.$_}}
            
            #create the PSObject
            $psobject = New-Object psobject -Property $objecthash

            #add the custom type name to the PSObject
            $psobject.PSObject.TypeNames.Insert(0,'Get.PortCertificate')         
            
            #save our certificate(s) to file if applicable
            if ($path){

                write-verbose "Saving certificate(s) to file."

                try {
                    $psobject.RawData | Set-Content -Encoding Byte -Path "$path\Cert`_$ComputerName`_$port`.cer" -ErrorAction stop
                    write-verbose "Certificate saved to $path\Cert`_$ComputerName`_$port`.cer."
                }
                catch{write-warning ("Unable to save certificate to {0}: {1}" -f "$path\Cert`_$ComputerName`_$port`.cer",$_.exception.message)}

                if($PSBoundParameters.ContainsKey('DownloadChain')){
                    
                    New-Item -ItemType directory -path "$path\ChainCerts`_$ComputerName`_$port" -ErrorAction SilentlyContinue > $null
                    
                    $psobject.chaincertificates.certificates | % {
                        try {
                            Set-Content $_.RawData -Encoding Byte -Path "$path\ChainCerts`_$ComputerName`_$port\$($_.thumbprint)`.cer" -ErrorAction stop
                            write-verbose "Certificate chain certificate saved to $path\ChainCerts`_$ComputerName`_$port\$($_.thumbprint)`.cer."
                        }
                        catch{
                            write-warning ("Unable to save certificate chain certificate to {0}: {1}" -f "$path\ChainCerts`_$ComputerName`_$port",$_.exception.message)
                        }
                    }
                }
            }

            #abort any connections
            $sslrequest.abort()

            #return the object
            $psobject
                              
        }

        else{
            #we were able to connect to the port but no ssl certificate was returned
            write-warning ("[{0}] No certificate returned on port {1}."-f $ComputerName,$Port)

            #abort any connections
            $sslrequest.abort()
            return $false 
        }
    }
}


$ComputerName = import-csv "C:\TEMP\Failed Comp.csv" | select -ExpandProperty computer
$PortArray = '22 TCP',
             '53 UDP',
             '80 TCP',
             '3389 TCP',
             '443 TCP'

$objServersList = @()


Foreach ($Computer in $ComputerName){
    $obj = @()
    $obj = new-object psobject
    $obj | add-member -name Computer -type noteproperty -value $Computer

    foreach ($Port in $PortArray){
                $Port1,$Port2 = $port.split(" ")

                if ($Port2 -eq 'TCP'){
                    

                    switch($Port1){
                    '3389'
                        {
                            $TestConnection = Test-NetConnection -ComputerName $Computer -Port $Port1
                            if($TestConnection.TcpTestSucceeded){
                                $Result = 'SUCCESS'
                                $obj | add-member -name $Port -type noteproperty -value $Result

                                $RdpCert = (Get-PortCertificate -ComputerName $Computer -port 3389) #
                                if($RdpCert -ne $false){ #sometimes 3389 is open but it's still unable to get a cert, this prevents it throwing an error.
                                    $RdpCertString = ($RdPCert.subject).trimstart('CN=')
                                    $obj | add-member -name 'RDP Cert' -type noteproperty -value $RdpCertString
                                }

                            }
                            else{
                                $Result = 'FAILURE'
                                $obj | add-member -name $Port -type noteproperty -value $result
                            }   

                        }
                    '443'
                        {
                            $TestConnection = Test-NetConnection -ComputerName $Computer -Port $Port1
                            if($TestConnection.TcpTestSucceeded){
                                $Result = 'SUCCESS'
                                $obj | add-member -name $Port -type noteproperty -value $Result

                                $SslCert =(Get-PortCertificate -ComputerName $Computer -port 443)
                                if($SslCert -ne $false){ #sometimes 443 is open but it's still unable to get a cert, this prevents it throwing an error.
                                    $SslCertString = ($SslCert.subject).replace(', ',' | ')
                                    $obj | add-member -name 'SSL Cert' -type noteproperty -value $SslCertString
                                }

                            }
                            else{
                                $Result = 'FAILURE'
                                $obj | add-member -name $Port -type noteproperty -value $result
                            }     

                        }
                    Default
                        {
                            $TestConnection = Test-NetConnection -ComputerName $Computer -Port $Port1
                            if($TestConnection.TcpTestSucceeded){
                                $Result = 'SUCCESS'
                                $obj | add-member -name $Port -type noteproperty -value $Result
                            }
                            else{
                                $Result = 'FAILURE'
                                $obj | add-member -name $Port -type noteproperty -value $Result
                            }



                        }
                    }





                }
                elseif($Port2 -eq 'UDP'){
                   $result = if((test-port -computer $computer -port $port1).open -eq $true){write-output "SUCCESS"}else{write-output "FAILURE"}
                   $obj | add-member -name $Port -type noteproperty -value $result

                }
                else{
                    $obj | add-member -name $Port -type noteproperty -value "Invalid protocol"
                }          
    }
$obj | ft
$objServersList+=$obj

}

$objServersList | ft

1

There are 1 answers

2
Mathias R. Jessen On BEST ANSWER

Format-Table decides the table header format based on the first few items piped to it, so if only some of the objects have an RDP Cert property and the first such object is not at the start of the list, it won't be shown.

The solution is to either always add the property to all objects regardless of whether it has a value or not, or by requesting Format-Table show the property explicitly:

$objServersList | ft Computer,'22 TCP','53 UDP','80 TCP','3389 TCP','RDP Cert','443 TCP'