Clear-Host # ====== Banner ====== Write-Host "==========================================" -ForegroundColor Cyan Write-Host " LANScan v1.6 - Network Tool " -ForegroundColor Yellow Write-Host " by Rich Kreider (c) 2025 " -ForegroundColor Cyan Write-Host "==========================================" -ForegroundColor Cyan # ====== Configuration Defaults ====== $Timeout = 200 $MaxThreads = 200 $NetBIOSThreads = 50 $DefaultPortRange = "1-1024" $PortList = @() $ShowClosed = $false $doPortScan = $false $doNetBIOS = $false $doCSV = $false $csvPath = $null $esc = [char]27 $colorMagenta = "${esc}[35m" $colorCyan = "${esc}[96m" $colorYellow = "${esc}[33m" $colorReset = "${esc}[0m" $OuiCachePath = Join-Path $env:TEMP "oui.csv" $OuiCacheAgeDays = 7 $OuiUrl = "https://standards-oui.ieee.org/oui/oui.csv" # ====== Helper Functions ====== function Expand-PortRange { param ([string]$RangeString) $expanded = @() $parts = $RangeString -split ',' | ForEach-Object { $_.Trim() } foreach ($part in $parts) { if ($part -match '^(\d+)-(\d+)$') { $expanded += [int]$matches[1]..[int]$matches[2] } elseif ($part -match '^\d+$') { $expanded += [int]$part } } return $expanded | Sort-Object -Unique } function Show-ScanSettings { Write-Host "`nDefault scan settings:" Write-Host " Timeout: $Timeout ms" Write-Host " Threads: $MaxThreads" Write-Host " Port Range: $DefaultPortRange" } function Prompt-ScanSettings { $modify = Read-Host "`nModify scan settings? (y/N)" if ($modify -match '^(y|yes)$') { $newTimeout = Read-Host "Timeout in ms (current: $Timeout)" if ($newTimeout -match '^\d+$') { $global:Timeout = [int]$newTimeout } $newThreads = Read-Host "Max threads (current: $MaxThreads)" if ($newThreads -match '^\d+$') { $global:MaxThreads = [int]$newThreads } $newRange = Read-Host "Port list (e.g., 22,80,1000-1100)" if ($newRange -match '^[\d,\- ]+$') { $global:PortList = Expand-PortRange $newRange } else { Write-Host "Invalid format. Using default range." -ForegroundColor Yellow $global:PortList = Expand-PortRange $DefaultPortRange } } else { $global:PortList = Expand-PortRange $DefaultPortRange } } function ConvertTo-CIDR { param ($netmask) if ($netmask -match '^\d+$') { return [int]$netmask } $binary = ($netmask -split '\.') | ForEach-Object { [Convert]::ToString([int]$_, 2).PadLeft(8, '0') } return ($binary -join '').ToCharArray() | Where-Object { $_ -eq '1' } | Measure-Object | Select-Object -ExpandProperty Count } function Get-HostRangeFromCIDR { param ($ip, $cidr) $ipAddr = [System.Net.IPAddress]::Parse($ip) $ipBytes = $ipAddr.GetAddressBytes() [Array]::Reverse($ipBytes) $ipInt = [BitConverter]::ToUInt32($ipBytes, 0) $mask = ([math]::Pow(2, 32) - [math]::Pow(2, (32 - $cidr))) $network = $ipInt -band [uint32]$mask $broadcast = $network + ([uint32]([math]::Pow(2, 32 - $cidr)) - 1) $ips = @() for ($i = $network + 1; $i -lt $broadcast; $i++) { $bytes = [BitConverter]::GetBytes($i) [Array]::Reverse($bytes) $ips += ([System.Net.IPAddress]::Parse(($bytes -join '.'))).ToString() } return $ips } function Sort-IPAddresses { param ([string[]]$IPs) return $IPs | Sort-Object { $octets = $_ -split '\.' [int]$octets[0] * 16777216 + [int]$octets[1] * 65536 + [int]$octets[2] * 256 + [int]$octets[3] } } # Returns array of open port numbers function Start-PortScan { param ([string]$TargetIP) try { $resolvedIP = [System.Net.Dns]::GetHostAddresses($TargetIP) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if (-not $resolvedIP) { throw "No IPv4 address found." } } catch { Write-Error "DNS resolution failed for ${TargetIP}: $_" return @() } $runspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads) $runspacePool.Open() $runspaces = @() foreach ($port in $PortList) { $ps = [powershell]::Create().AddScript({ param($ip, $port, $timeout) try { $endpoint = New-Object System.Net.IPEndPoint($ip, $port) $socket = New-Object System.Net.Sockets.Socket( [System.Net.Sockets.AddressFamily]::InterNetwork, [System.Net.Sockets.SocketType]::Stream, [System.Net.Sockets.ProtocolType]::Tcp ) $iar = $socket.BeginConnect($endpoint, $null, $null) $connected = $iar.AsyncWaitHandle.WaitOne($timeout, $false) if ($connected -and $socket.Connected) { $socket.EndConnect($iar); $socket.Close() return [PSCustomObject]@{ Port = $port; Open = $true } } else { $socket.Close() return [PSCustomObject]@{ Port = $port; Open = $false } } } catch { return [PSCustomObject]@{ Port = $port; Open = $false } } }).AddArgument($resolvedIP).AddArgument($port).AddArgument($Timeout) $ps.RunspacePool = $runspacePool $runspaces += [PSCustomObject]@{ Pipe = $ps; Async = $ps.BeginInvoke() } } $results = @() foreach ($r in $runspaces) { $result = $r.Pipe.EndInvoke($r.Async) $r.Pipe.Dispose() $results += $result } $runspacePool.Close() $runspacePool.Dispose() if ($ShowClosed) { foreach ($r in ($results | Where-Object { -not $_.Open })) { Write-Host " -> Port $($r.Port) is CLOSED on $TargetIP" } } return @($results | Where-Object { $_.Open } | Select-Object -ExpandProperty Port) } # Cascading hostname resolution: DNS -> nbtstat -> WMI/CIM function Start-HostnameResolution { param ([string[]]$IPs) $nbPool = [runspacefactory]::CreateRunspacePool(1, $NetBIOSThreads) $nbPool.Open() $nbRunspaces = @() $resolveScript = { param([string]$IP) $result = [PSCustomObject]@{ IP = $IP Hostname = $null NBName = $null NBWorkgroup = $null Method = $null } try { $entry = [System.Net.Dns]::GetHostEntry($IP) if ($entry.HostName -and $entry.HostName -ne $IP) { $result.Hostname = $entry.HostName $result.Method = 'DNS' return $result } } catch {} try { $nbt = & nbtstat.exe -A $IP 2>$null if ($nbt) { $nameLine = $nbt | Where-Object { $_ -match '<00>\s+UNIQUE' } | Select-Object -First 1 if ($nameLine -match '^\s*(\S+)\s+<00>') { $result.NBName = $matches[1].Trim() $result.Hostname = $result.NBName $result.Method = 'NetBIOS' } $wgLine = $nbt | Where-Object { $_ -match '<00>\s+GROUP' } | Select-Object -First 1 if ($wgLine -match '^\s*(\S+)\s+<00>') { $result.NBWorkgroup = $matches[1].Trim() } if ($result.Hostname) { return $result } } } catch {} try { $cimOpts = New-CimSessionOption -Protocol Dcom $session = New-CimSession -ComputerName $IP -SessionOption $cimOpts ` -OperationTimeoutSec 3 -ErrorAction Stop $cim = Get-CimInstance -CimSession $session -ClassName Win32_ComputerSystem ` -ErrorAction Stop Remove-CimSession $session -ErrorAction SilentlyContinue if ($cim.DNSHostName) { $result.Hostname = $cim.DNSHostName $result.Method = 'WMI' $result.NBWorkgroup = if ($cim.PartOfDomain) { $cim.Domain } else { $cim.Workgroup } } } catch {} return $result } foreach ($ip in $IPs) { $ps = [powershell]::Create().AddScript($resolveScript).AddArgument($ip) $ps.RunspacePool = $nbPool $nbRunspaces += [PSCustomObject]@{ Pipe = $ps; Async = $ps.BeginInvoke(); IP = $ip } } $hostnameMap = @{} $nbTotal = $nbRunspaces.Count $nbCount = 0 foreach ($r in $nbRunspaces) { try { $res = $r.Pipe.EndInvoke($r.Async) if ($res) { $hostnameMap[$r.IP] = $res } } catch {} finally { $r.Pipe.Dispose() } $nbCount++ Write-Progress -Activity "Resolving Hostnames" ` -Status "$nbCount of $nbTotal" ` -PercentComplete ([int](($nbCount / $nbTotal) * 100)) } $nbPool.Close() $nbPool.Dispose() Write-Progress -Activity "Resolving Hostnames" -Completed return $hostnameMap } # Downloads OUI CSV from IEEE if missing or stale, returns hashtable keyed by normalized OUI prefix function Get-OuiTable { $download = $true if (Test-Path $OuiCachePath) { $age = (Get-Date) - (Get-Item $OuiCachePath).LastWriteTime if ($age.TotalDays -lt $OuiCacheAgeDays) { Write-Host "Using cached OUI database ($([math]::Floor($age.TotalDays))d old)." -ForegroundColor Cyan $download = $false } else { Write-Host "OUI cache is $([math]::Floor($age.TotalDays))d old — refreshing..." -ForegroundColor Yellow } } else { Write-Host "No OUI cache found — downloading..." -ForegroundColor Yellow } if ($download) { try { Invoke-WebRequest -Uri $OuiUrl -OutFile $OuiCachePath -UseBasicParsing -UserAgent "LANscan" Write-Host "OUI database cached to $OuiCachePath" -ForegroundColor Cyan } catch { Write-Warning "Failed to download OUI database: $_" # If a stale cache exists, fall back to it rather than failing completely if (-not (Test-Path $OuiCachePath)) { return @{} } Write-Host "Falling back to stale OUI cache." -ForegroundColor Yellow } } $ouiTable = @{} try { Import-Csv $OuiCachePath | ForEach-Object { # IEEE CSV columns: Registry,Assignment,Organization Name,Organization Address $key = $_.Assignment.ToUpper() -replace '[^0-9A-F]', '' if ($key.Length -eq 6 -and -not $ouiTable.ContainsKey($key)) { $ouiTable[$key] = $_.'Organization Name'.Trim() } } Write-Host "$($ouiTable.Count) OUI entries loaded." -ForegroundColor Cyan } catch { Write-Warning "Failed to parse OUI database: $_" } return $ouiTable } function Get-Vendor { param ([string]$MAC, [hashtable]$OuiTable) # Normalize MAC to 6 uppercase hex chars (first 3 octets) $normalized = $MAC.ToUpper() -replace '[^0-9A-F]', '' if ($normalized.Length -lt 6) { return '' } $oui = $normalized.Substring(0, 6) if ($OuiTable.ContainsKey($oui)) { return $OuiTable[$oui] } return '' } # ====== Initial Prompts ====== $portInput = Read-Host "`nRun port scan on discovered hosts? (y/N)" if ($portInput -match '^(y|yes)$') { $doPortScan = $true Show-ScanSettings Prompt-ScanSettings } $nbInput = Read-Host "Run NetBIOS/hostname lookups on discovered hosts? (y/N)" if ($nbInput -match '^(y|yes)$') { $doNetBIOS = $true } $csvInput = Read-Host "Save results to CSV? (y/N)" if ($csvInput -match '^(y|yes)$') { $doCSV = $true $csvPath = Join-Path $PWD "lanscan_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" Write-Host " -> Output: $csvPath" -ForegroundColor Cyan } # ====== OUI Database ====== Write-Host "`nLoading OUI vendor database..." -ForegroundColor Green $ouiTable = Get-OuiTable # ====== Network Discovery ====== Write-Host "`nStarting network discovery..." -ForegroundColor Green $ipsToPing = @() $manualMode = $false $manualEntry = Read-Host "Enter a manual IP and CIDR/netmask? (y/N)" if ($manualEntry -match '^(y|yes)$') { $manualMode = $true $ipInput = Read-Host "Enter IP address (e.g., 192.168.1.0)" $maskInput = Read-Host "Enter CIDR or netmask (e.g., 24 or 255.255.255.0)" $cidr = ConvertTo-CIDR $maskInput if (-not $cidr -or $cidr -lt 0 -or $cidr -gt 32) { Write-Host "Invalid subnet mask or CIDR." -ForegroundColor Red exit } $ipsToPing = Get-HostRangeFromCIDR $ipInput $cidr } else { $adapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.PrefixOrigin -in ('Dhcp', 'Manual') -and $_.InterfaceAlias -notmatch 'Loopback' } if (-not $adapters) { Write-Host "No active network adapters found." -ForegroundColor Red exit } foreach ($adapter in $adapters) { $baseIP = $adapter.IPAddress -replace '\.\d+$', '.' $ipsToPing += (1..254 | ForEach-Object { "$baseIP$_" }) } } # ====== Ping Sweep ====== $pingPool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads) $pingPool.Open() $pingRunspaces = @() foreach ($ip in $ipsToPing) { $pingJob = [powershell]::Create().AddScript({ param($ip, $timeout) $ping = New-Object System.Net.NetworkInformation.Ping try { $null = $ping.Send($ip, $timeout) } catch {} }).AddArgument($ip).AddArgument($Timeout) $pingJob.RunspacePool = $pingPool $pingRunspaces += [PSCustomObject]@{ Pipe = $pingJob; Handle = $pingJob.BeginInvoke() } } $activeIPs = @() $totalIPs = $ipsToPing.Count $currentCount = 0 foreach ($r in $pingRunspaces) { if ($null -eq $r -or $null -eq $r.Pipe) { continue } try { [void]$r.Pipe.EndInvoke($r.Handle) while ($r.Pipe.Output.Count -gt 0) { $ipString = $r.Pipe.Output.Read() if ($ipString) { $activeIPs += $ipString.ToString() } } } catch { Write-Warning "Ping error at index ${currentCount}: $_" } finally { $r.Pipe.Dispose() } $currentCount++ Write-Progress -Activity "Pinging Hosts" ` -Status "IP $currentCount of ${totalIPs}: $($ipsToPing[$currentCount - 1])" ` -PercentComplete ([int](($currentCount / $totalIPs) * 100)) } $pingPool.Close() $pingPool.Dispose() Write-Progress -Activity "Pinging Hosts" -Completed # ====== ARP Table Lookup ====== $arp = arp -a $ipMacMap = @{} $manualSubnetPrefix = if ($manualMode) { $ipInput -replace '\.\d+$', '.' } else { $null } foreach ($line in $arp) { if ($line -match '(\d{1,3}(\.\d{1,3}){3})\s+([a-fA-F0-9:-]{17})') { $ip = $matches[1] $mac = $matches[3] if ($manualMode) { if ($ip.StartsWith($manualSubnetPrefix)) { $ipMacMap[$ip] = $mac } } else { $ipMacMap[$ip] = $mac } } } foreach ($ip in $activeIPs) { if (-not $ipMacMap.ContainsKey($ip)) { $ipMacMap[$ip] = 'N/A' } } if ($ipMacMap.Count -eq 0) { Write-Host "No hosts discovered." -ForegroundColor Yellow exit } Write-Host "$($ipMacMap.Count) host(s) discovered." -ForegroundColor Green # ====== Hostname Resolution ====== $hostnameMap = @{} if ($doNetBIOS) { Write-Host "`nResolving hostnames (DNS -> NetBIOS -> WMI)..." -ForegroundColor Green $hostnameMap = Start-HostnameResolution -IPs ([string[]]$ipMacMap.Keys) $resolved = ($hostnameMap.Values | Where-Object { $_.Hostname }).Count Write-Host "$resolved of $($ipMacMap.Count) host(s) resolved." -ForegroundColor Green } # ====== Classify IP ====== function Get-IPType { param([string]$IP) $first = [int]($IP -split '\.')[0] if ($IP -eq '255.255.255.255' -or $IP -match '\.255$') { return 'broadcast' } if ($first -ge 224 -and $first -le 239) { return 'multicast' } return 'unicast' } # ====== Build and Display Output ====== Write-Host "`nDiscovered Hosts:" -ForegroundColor Green $csvRows = [System.Collections.Generic.List[PSCustomObject]]::new() $sortedIPs = Sort-IPAddresses -IPs ([string[]]$ipMacMap.Keys) if ($doPortScan) { Write-Host "Starting port scan on discovered hosts..." -ForegroundColor Green } $hostTotal = $sortedIPs.Count $hostCount = 0 $separatorWritten = $false Write-Host "" Write-Host " +------------------+-------------------+----------------------------+" -ForegroundColor Cyan Write-Host " | IP Address | MAC Address | Vendor |" -ForegroundColor Cyan Write-Host " +------------------+-------------------+----------------------------+" -ForegroundColor Cyan foreach ($ip in $sortedIPs) { $mac = $ipMacMap[$ip] $ipType = Get-IPType $ip $vendor = Get-Vendor -MAC $mac -OuiTable $ouiTable if (-not $vendor) { if ($ipType -eq 'multicast') { $vendor = 'Multicast' } elseif ($ipType -eq 'broadcast') { $vendor = 'Broadcast' } else { $vendor = 'Unknown' } } $hn = $hostnameMap[$ip] $hostname = if ($hn -and $hn.Hostname) { $hn.Hostname } else { '' } $nbName = if ($hn -and $hn.NBName) { $hn.NBName } else { '' } $nbWorkgroup = if ($hn -and $hn.NBWorkgroup) { $hn.NBWorkgroup } else { '' } $method = if ($hn -and $hn.Method) { $hn.Method } else { '' } # Port scan $openPortNums = @() if ($doPortScan) { $hostCount++ Write-Progress -Activity "Port Scanning Hosts" ` -Status "Scanning $ip ($hostCount of $hostTotal)" ` -PercentComplete ([int](($hostCount / $hostTotal) * 100)) $openPortNums = Start-PortScan $ip } # Separator before first multicast/broadcast block if ($ipType -ne 'unicast' -and -not $separatorWritten) { Write-Host " +------------------+-------------------+----------------------------+" -ForegroundColor DarkGray $separatorWritten = $true } if ($ipType -ne 'unicast') { # Dimmed row $hostSuffix = if ($hostname) { " $hostname" } else { '' } Write-Host " | $ip" -ForegroundColor DarkGray -NoNewline Write-Host " $mac $vendor$hostSuffix" -ForegroundColor DarkGray } else { # Normal colored row $hostSuffix = if ($hostname) { "" } else { '' } Write-Host " | " -ForegroundColor Cyan -NoNewline Write-Host $ip -ForegroundColor Cyan -NoNewline Write-Host " $mac " -NoNewline Write-Host $vendor -ForegroundColor Yellow -NoNewline if ($hostname) { Write-Host " " -NoNewline Write-Host $hostname -ForegroundColor Magenta -NoNewline if ($nbWorkgroup) { Write-Host " ($nbWorkgroup)" -ForegroundColor DarkGray -NoNewline } if ($method) { Write-Host " [$method]" -ForegroundColor DarkYellow -NoNewline } } Write-Host "" if ($openPortNums.Count -gt 0) { Write-Host " | +-- ports: $($openPortNums -join ' ')" -ForegroundColor Cyan } } if ($doCSV) { $csvRows.Add([PSCustomObject]@{ IP = $ip MAC = $mac Vendor = $vendor Hostname = $hostname NBName = $nbName NBWorkgroup = $nbWorkgroup Method = $method OpenPorts = ($openPortNums -join ';') }) } } if ($doPortScan) { Write-Progress -Activity "Port Scanning Hosts" -Completed } Write-Host " +------------------+-------------------+----------------------------+" -ForegroundColor Cyan # ====== CSV Export ====== if ($doCSV -and $csvRows.Count -gt 0) { try { $csvRows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8 Write-Host "`nResults saved to: $csvPath" -ForegroundColor Cyan } catch { Write-Warning "Failed to save CSV: $_" } } elseif ($doCSV) { Write-Host "`nNo rows to export." -ForegroundColor Yellow }