Error adding object to array

Hello,

I have simple script to scan open ports on remote computers and it works quite well if I scan small amount of ports. But when I want to scan full range - from 0 to 65535 - it ends with error:

Line |
37 | $HashTableFortiGateResults += $HashTableFortiGateObject
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named ‘op_Addition’.

Also when it scan small amount of ports, Export-Csv -Path “ble.csv” saves all scan results, but when it scan full range ports, only result from first scanned computers is saved…

Script is below:

$TCPportRange = @(1..65535)
#$TCPPortRange = @(443,4433,10443)

# Reading from file computers to scan and omit those beginning with #
$ComputersToScan = (Get-Content -Path .\ComputersToScan.csv | ForEach-Object {if ($_ -notmatch "^#.+"){$_}} | ConvertFrom-CSV)

$global:HashTableFortiGateResults = @() # This table will store object made from FortiGates names and IP's. It has global scope to allow user access to it from environment after script finish its job.
$CurrentScanResult = @() # This table will store scan result of one court (all ports, but only one court)
#$AllScanResults = @() # This table will store scan results of all courts (all courts with all scanned ports)


# Creating objects containing FortiGates Names and IP's
ForEach ($Computer in $ComputersToScan){
    $OpenPorts = @{OpenPorts = @()}
    $ScanTimeStart = Get-Date
    #$PingReply = ((Test-NetConnection -ComputerName $Computer.IP).PingSucceeded)
    $PingReply = (Test-Connection -ComputerName $Computer.IP -Quiet -Count 1)

    $TCPPortRange | ForEach-Object -Parallel {
        write-host "skanuje $($using:Computer.Description) $($using:Computer.IP):$_ "

        if ($TRUE -eq (Test-NetConnection -ComputerName $($using:Computer.IP) -Port $_).TcpTestSucceeded){
            write-host $FortiGateHostName $_ $($CurrentScanResult.TcpTestSucceeded)
            ($using:OpenPorts).OpenPorts += $_
        }
    }  -ThrottleLimit 10000
    
    $HashTableFortiGateObject = New-Object -TypeName PSCustomObject -Property @{
        ComputerDescription = $Computer.Description
        ComputerIP = $Computer.IP
        OpenPorts = $OpenPorts.OpenPorts
        ScanTimeStart = $ScanTimeStart
        AllowedPorts = $Computer.AllowedPorts
        ScanTimeEnd = Get-Date
        PingReply = $PingReply
    }
    $HashTableFortiGateResults += $HashTableFortiGateObject
    $HashTableFortiGateResults | Select-Object ComputerDescription, ComputerIP, @{N="OpenPorts";E={$_.OpenPorts -join ";"}}, ScanTimeStart, AllowedPorts, ScanTimeEnd, PingReply | Export-Csv -Path "ble.csv"
}

$HashTableFortiGateResults | format-table

Does anyone can help me understand what is wrong with that code?

Best Regards

I can’t see anything that would give that error and I can’t force the error with the limited testing I’ve done.

However, I would recommend that you try switching out the simple Array for a Generic List and avoid the use of +=. This is generally considered a best practice because simple arrays do not scale well.

You can declare it like this:

$HashTableFortiGateResults = [System.Collections.Generic.List[Object]]:New()

I think it’s confusing to call the array $HashTableFortiGateResults even if it is an array of Hashtables but I’ve left your variable name in the example :slight_smile:

To add to a Generic List use the Add() method.

$HashTableFortiGateResults.Add($HashTableFortiGateObject)

Regarding your second problem of overwriting the CSV file, that’s not due to the number of ports you’re scanning but the number of computers. Because it’s inside your loop, it’s overwritten for each computer. You can move it outside the loop or, as you’re using version 7+, you can use the -Append parameter to keep adding data to your CSV file.

Final point, it might just be my clunky old machine but this was super slow. Have you considered using nmap for this :grinning:

1 Like

I think the issue may be with the foreach -parallel as you need to use thread safe dictionaries
PowerShell ForEach-Object Parallel Feature | PowerShell Team (microsoft.com)

Would look something like this

ForEach ($Computer in $ComputersToScan) {
    $OpenPorts = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()
    $ScanTimeStart = Get-Date
    #$PingReply = ((Test-NetConnection -ComputerName $Computer.IP).PingSucceeded)
    $PingReply = (Test-Connection -ComputerName $Computer.IP -Quiet -Count 1)

    $TCPPortRange | ForEach-Object -Parallel {
        write-host "skanuje $($using:Computer.Description) $($using:Computer.IP):$portNumber "

        if ($TRUE -eq (Test-NetConnection -ComputerName $($using:Computer.IP) -Port $_).TcpTestSucceeded) {
            write-host $FortiGateHostName $_ $($CurrentScanResult.TcpTestSucceeded)
            # Need to link to the dictionary, you can add directly to the $using scope
            $dictionaryLink = $using:OpenPorts
            [void]$dictionaryLink.TryAdd('$_','Open')
        }
    }  -ThrottleLimit 10000

$HashTableFortiGateObject = New-Object -TypeName PSCustomObject -Property @{
        ComputerDescription = $Computer.Description
        ComputerIP          = $Computer.IP
        #Need the reference the keys of the dictionary
        OpenPorts           = $OpenPorts.Keys
        ScanTimeStart       = $ScanTimeStart
        AllowedPorts        = $Computer.AllowedPorts
        ScanTimeEnd         = Get-Date
        PingReply           = $PingReply
    }

As above [System.Collections.Generic.List[Object]]:New() will give better performance on large numbers of computers. <1000 the += is fine but as it redraws the entire array it does start to get slower the more it has to do while lists you can just add the single object and doesn’t degrade.

Not used it myself much but I do think nmap is the right tool for this particular job

1 Like

Ok. I don’t fully understand why this helped, but I changed two things:
Line:

$global:HashTableFortiGateResults = @() # This table will store object made from FortiGates

was changed to (“global” scope modifier was removed):

$ArrayFortiGateResults  = @() # This table will store object made from FortiGates

and in below lines was changed (in below lines only variable names was changed)

$HashTableFortiGateResults += $HashTableFortiGateObject
$HashTableFortiGateResults | Select-Object ComputerDescription, ComputerIP, @{N="OpenPorts";E={$_.OpenPorts -join ";"}}, ScanTimeStart, AllowedPorts, ScanTimeEnd, PingReply | Export-Csv -Path "ble.csv"
}
$HashTableFortiGateResults | format-table

into:

$ArrayFortiGateResults += $HashTableFortiGateObject
$ArrayFortiGateResults | Select-Object ComputerDescription, ComputerIP, @{N="OpenPorts";E={$_.OpenPorts -join ";"}}, ScanTimeStart, AllowedPorts, ScanTimeEnd, PingReply | Export-Csv -Path "ble.csv"
}
$ArrayFortiGateResults | format-table

Now its working fine :slight_smile: