summaryrefslogtreecommitdiff
path: root/lanscan
diff options
context:
space:
mode:
Diffstat (limited to 'lanscan')
-rw-r--r--lanscan571
1 files changed, 571 insertions, 0 deletions
diff --git a/lanscan b/lanscan
new file mode 100644
index 0000000..316283a
--- /dev/null
+++ b/lanscan
@@ -0,0 +1,571 @@
+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
+}
+