Public/Get-VBTCPFingerprint.ps1
|
function Get-VBTCPFingerprint { <# .SYNOPSIS Scan 24 well-known ports concurrently to fingerprint a device (Layer 5). .DESCRIPTION Probes all 24 ports in parallel using async BeginConnect + WaitOne on a single IP. Total wall time is approximately equal to the timeout value regardless of how many ports are scanned -- the connections race concurrently. All TcpClient objects are released in a finally block on every code path. Port list (24 ports) as defined in the module design spec: 22 SSH 5060 SIP 23 Telnet 5061 SIP/TLS 80 HTTP 5985 WinRM HTTP 135 RPC 8000 Alt HTTP 161 SNMP 8080 Alt HTTP 443 HTTPS 8443 Alt HTTPS 445 SMB 9100 JetDirect 515 LPD 9443 VMware 548 AFP 37777 Dahua NVR 554 RTSP 62078 iOS 631 IPP 1883 MQTT 902 VMware 3389 RDP Prerequisites: $Context.NetworkProbeEnabled must be $true. .PARAMETER IPAddress The RFC1918 / CGNAT / link-local IP address to scan. .PARAMETER Context Environment context from Get-VBEnrichmentContext. Provides NetworkProbeEnabled and DefaultTimeoutMs.TCP. .PARAMETER TimeoutMs Per-port connection timeout in milliseconds. Defaults to $Context.DefaultTimeoutMs.TCP (300 ms). Override here for slower networks. .OUTPUTS [PSCustomObject] -- base layer result fields plus: OpenPorts [string] comma-separated open port numbers OpenPortsList [int[]] array of open port numbers ScanDurationMs [int] .EXAMPLE $ctx = Get-VBEnrichmentContext Get-VBTCPFingerprint -IPAddress '192.168.1.45' -Context $ctx .EXAMPLE '192.168.1.45','192.168.1.46' | Get-VBTCPFingerprint -Context $ctx .NOTES Version: 1.0.0 MinPSVersion: 5.1 Author: VB ChangeLog: 1.0.0 -- 2026-05-11 -- Initial release #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$IPAddress, [Parameter()] [PSCustomObject]$Context, [Parameter()] [int]$TimeoutMs = 1000 ) begin { $LAYER_NUM = 5 $LAYER_NAME = 'TCP' if (-not $Context) { Write-Warning "[$LAYER_NAME] No context provided -- running without prerequisite validation." } # Resolve timeout from context if not explicitly overridden if ($PSBoundParameters.ContainsKey('TimeoutMs') -eq $false -and $Context -and $Context.DefaultTimeoutMs) { $TimeoutMs = $Context.DefaultTimeoutMs.TCP } $FingerprintPorts = @( 22, 23, 80, 135, 161, 443, 445, 515, 548, 554, 631, 902, 1883, 3389, 5060, 5061, 5985, 8000, 8080, 8443, 9100, 9443, 37777, 62078 ) } process { $sw = [System.Diagnostics.Stopwatch]::StartNew() if ($Context -and -not $Context.NetworkProbeEnabled) { $sw.Stop() return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'Skipped' -ExecutionMs $sw.ElapsedMilliseconds ` -SkipReason 'NetworkProbeDisabled' ` -Impact 'No port-based device class signals available' } $clients = @() $handles = @() try { # Launch all connections concurrently foreach ($port in $FingerprintPorts) { $client = New-Object System.Net.Sockets.TcpClient $clients += $client try { $handle = $client.BeginConnect($IPAddress, $port, $null, $null) $handles += [PSCustomObject]@{ Port = $port; Client = $client; Handle = $handle } } catch { # BeginConnect itself failed (bad IP format etc) -- skip this port $handles += [PSCustomObject]@{ Port = $port; Client = $client; Handle = $null } } } # Wait for all and collect open ports $openPorts = New-Object System.Collections.Generic.List[int] foreach ($entry in $handles) { if ($null -eq $entry.Handle) { continue } try { $completed = $entry.Handle.AsyncWaitHandle.WaitOne($TimeoutMs, $false) if ($completed -and $entry.Client.Connected) { $openPorts.Add($entry.Port) } } catch { # Connection refused or timeout -- port closed, ignore } } $sw.Stop() if ($openPorts.Count -eq 0) { return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'NoResult' -ExecutionMs $sw.ElapsedMilliseconds ` -ExtraFields @{ OpenPorts = '' OpenPortsList = @() ScanDurationMs = [int]$sw.ElapsedMilliseconds } } $sortedPorts = @($openPorts | Sort-Object) New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'Success' -ExecutionMs $sw.ElapsedMilliseconds ` -ExtraFields @{ OpenPorts = $sortedPorts -join ',' OpenPortsList = $sortedPorts ScanDurationMs = [int]$sw.ElapsedMilliseconds } } catch { $sw.Stop() New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'Failed' -ExecutionMs $sw.ElapsedMilliseconds ` -ErrorDetail $_.Exception.Message } finally { # Always release all TcpClient objects foreach ($client in $clients) { try { $client.Close() } catch { } try { $client.Dispose() } catch { } } } } } |