Last active
April 30, 2026 00:56
-
-
Save comoc/9611de85d25bbbda632bd4e611565490 to your computer and use it in GitHub Desktop.
LAN内機器の種別を推定するPowerShellスクリプト (Shift-JISかUTF-8+BOMで保存すること)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| LAN内機器の種別を推定するスクリプト (既定は画面出力のみ) | |
| .DESCRIPTION | |
| 既定では Get-NetNeighbor から LAN 内機器を取得し、 | |
| OUI ベンダー、TTL からの OS 推定、NetBIOS 名、代表ポート開閉、 | |
| dns-sd.exe (利用可能時) による mDNS/Bonjour 機器名を取得して、 | |
| DeviceType (機器カテゴリ推定) を付与し画面表示する。 | |
| -InputCsv 指定時は Get-LanDevices.ps1 が出した CSV を入力として使う。 | |
| -SaveCsv または -OutputPath 指定時のみ CSV 保存。 | |
| .PARAMETER InputCsv | |
| Get-LanDevices.ps1 が出した CSV のパス。指定すると CSV を入力に使う。 | |
| 省略時は Get-NetNeighbor から直接取得 (既定動作)。 | |
| .PARAMETER Discover | |
| 後方互換用 (現在は既定動作と同じため指定しなくても可)。 | |
| .PARAMETER SaveCsv | |
| 既定パス (カレント\lan-identified_<timestamp>.csv) に CSV 保存する。 | |
| .PARAMETER OutputPath | |
| 出力 CSV パスを明示。指定すると暗黙的に保存ON。 | |
| .PARAMETER ScanPorts | |
| 代表ポート (22,80,443,445,515,631,8009,9100,32400 ほか) のスキャンを行う。既定 ON。 | |
| .PARAMETER UseOnlineOuiLookup | |
| 内蔵 OUI 表に無いものを api.macvendors.com に問い合わせる。 | |
| .PARAMETER NoMdns | |
| dns-sd.exe が利用可能でも mDNS 検索を行わない。 | |
| .PARAMETER MdnsBrowseSeconds | |
| mDNS browse の待機秒数。既定 3。 | |
| .PARAMETER PortTimeoutMs | |
| 1ポートあたりの接続タイムアウト [ms]。既定 300。 | |
| .EXAMPLE | |
| .\Identify-LanDevices.ps1 | |
| 既定動作: Get-NetNeighbor + (利用可能なら) mDNS で取得し画面表示 | |
| .EXAMPLE | |
| .\Identify-LanDevices.ps1 -UseOnlineOuiLookup -SaveCsv | |
| オンラインOUI検索 + CSV保存 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [string]$InputCsv, | |
| [switch]$Discover, | |
| [switch]$SaveCsv, | |
| [string]$OutputPath, | |
| [bool]$ScanPorts = $true, | |
| [switch]$UseOnlineOuiLookup, | |
| [switch]$NoMdns, | |
| [int]$MdnsBrowseSeconds = 3, | |
| [int]$PortTimeoutMs = 300, | |
| [int]$PingTimeoutMs = 400 | |
| ) | |
| #------------------------------------------------------------------------------- | |
| # 内蔵 OUI 表 | |
| #------------------------------------------------------------------------------- | |
| $script:InternalOui = @{ | |
| 'B827EB' = 'Raspberry Pi Foundation' | |
| 'DCA632' = 'Raspberry Pi Trading' | |
| 'E45F01' = 'Raspberry Pi Trading' | |
| '28CDC1' = 'Raspberry Pi Trading' | |
| 'D83ADD' = 'Raspberry Pi Trading' | |
| '2CCF67' = 'Raspberry Pi Trading' | |
| '240AC4' = 'Espressif' | |
| '30AEA4' = 'Espressif' | |
| '7C9EBD' = 'Espressif' | |
| 'A020A6' = 'Espressif' | |
| '8CAAB5' = 'Espressif' | |
| '840D8E' = 'Espressif' | |
| 'C45BBE' = 'Espressif' | |
| 'C8C9A3' = 'Espressif' | |
| '94B97E' = 'Espressif' | |
| '005056' = 'VMware' | |
| '000C29' = 'VMware' | |
| '001C14' = 'VMware' | |
| '00155D' = 'Microsoft Hyper-V' | |
| '080027' = 'VirtualBox (Oracle)' | |
| '0A0027' = 'VirtualBox (Oracle)' | |
| '7C1E52' = 'Microsoft' | |
| '286521' = 'Microsoft' | |
| '0019FD' = 'Nintendo' | |
| '7CBB8A' = 'Nintendo' | |
| 'CC9E00' = 'Nintendo' | |
| '00074D' = 'BUFFALO' | |
| '4CE676' = 'BUFFALO' | |
| '9CA525' = 'BUFFALO' | |
| '00601D' = 'I-O DATA' | |
| '009095' = 'I-O DATA' | |
| '0008EE' = 'NEC' | |
| '00007A' = 'NEC' | |
| '08007A' = 'Sharp' | |
| 'B0BAC4' = 'Panasonic' | |
| '402CF4' = 'Panasonic' | |
| '001132' = 'Synology' | |
| '244BFE' = 'QNAP' | |
| '24E4A8' = 'QNAP' | |
| } | |
| $script:OuiOnlineCache = @{} | |
| #------------------------------------------------------------------------------- | |
| # OUI ベンダー解決 | |
| #------------------------------------------------------------------------------- | |
| function Get-OuiVendor { | |
| param([string]$MacAddress, [switch]$UseOnline) | |
| if (-not $MacAddress) { return '' } | |
| $oui = ($MacAddress -replace '[-:]', '').ToUpper() | |
| if ($oui.Length -lt 6) { return '' } | |
| $oui = $oui.Substring(0, 6) | |
| $firstByte = [Convert]::ToInt32($oui.Substring(0, 2), 16) | |
| if (($firstByte -band 0x02) -eq 0x02) { | |
| return '(Locally administered)' | |
| } | |
| if ($script:InternalOui.ContainsKey($oui)) { return $script:InternalOui[$oui] } | |
| if ($script:OuiOnlineCache.ContainsKey($oui)) { return $script:OuiOnlineCache[$oui] } | |
| if ($UseOnline) { | |
| try { | |
| $url = "https://api.macvendors.com/{0}" -f $oui | |
| $vendor = Invoke-RestMethod -Uri $url -TimeoutSec 5 -ErrorAction Stop | |
| $script:OuiOnlineCache[$oui] = $vendor | |
| Start-Sleep -Milliseconds 1100 | |
| return $vendor | |
| } catch { | |
| $script:OuiOnlineCache[$oui] = '' | |
| return '' | |
| } | |
| } | |
| return '' | |
| } | |
| #------------------------------------------------------------------------------- | |
| # Ping TTL → OS 推定 | |
| #------------------------------------------------------------------------------- | |
| function Get-PingTtl { | |
| param([string]$IPAddress, [int]$TimeoutMs = 400) | |
| try { | |
| $ping = New-Object System.Net.NetworkInformation.Ping | |
| $reply = $ping.Send($IPAddress, $TimeoutMs) | |
| if ($reply.Status -eq 'Success') { | |
| $ttl = $reply.Options.Ttl | |
| $osGuess = switch ($true) { | |
| ($ttl -ge 250) { 'Embedded/Cisco (TTL=255)' ; break } | |
| (($ttl -ge 110) -and ($ttl -le 128)) { 'Windows (TTL=128)' ; break } | |
| (($ttl -ge 60) -and ($ttl -le 64)) { 'Linux/macOS/iOS/Android (TTL=64)' ; break } | |
| default { "TTL=$ttl" } | |
| } | |
| return [PSCustomObject]@{ Ttl = $ttl; OsGuess = $osGuess } | |
| } | |
| } catch { } | |
| return [PSCustomObject]@{ Ttl = $null; OsGuess = '' } | |
| } | |
| #------------------------------------------------------------------------------- | |
| # NetBIOS 名取得 | |
| #------------------------------------------------------------------------------- | |
| function Get-NetBIOSName { | |
| param([string]$IPAddress) | |
| try { | |
| $output = & nbtstat.exe -A $IPAddress 2>$null | |
| if (-not $output) { return '' } | |
| $line = $output | Select-String -Pattern '<00>\s+UNIQUE' | Select-Object -First 1 | |
| if ($line) { | |
| return ($line.Line.Trim() -split '\s+')[0] | |
| } | |
| } catch { } | |
| return '' | |
| } | |
| #------------------------------------------------------------------------------- | |
| # ポートスキャン | |
| #------------------------------------------------------------------------------- | |
| $script:CommonPorts = @( | |
| @{ Port = 22; Service = 'SSH' }, | |
| @{ Port = 80; Service = 'HTTP' }, | |
| @{ Port = 139; Service = 'NetBIOS' }, | |
| @{ Port = 443; Service = 'HTTPS' }, | |
| @{ Port = 445; Service = 'SMB' }, | |
| @{ Port = 515; Service = 'LPD' }, | |
| @{ Port = 548; Service = 'AFP' }, | |
| @{ Port = 631; Service = 'IPP' }, | |
| @{ Port = 3389; Service = 'RDP' }, | |
| @{ Port = 5000; Service = 'UPnP/Synology' }, | |
| @{ Port = 7000; Service = 'AirPlay' }, | |
| @{ Port = 8009; Service = 'Cast' }, | |
| @{ Port = 9100; Service = 'RAW-Print' }, | |
| @{ Port = 32400; Service = 'Plex' } | |
| ) | |
| function Test-OpenPorts { | |
| param([string]$IPAddress, [int]$TimeoutMs = 300) | |
| $tasks = foreach ($p in $script:CommonPorts) { | |
| $client = New-Object System.Net.Sockets.TcpClient | |
| [PSCustomObject]@{ | |
| Port = $p.Port | |
| Service = $p.Service | |
| Client = $client | |
| Task = $client.ConnectAsync($IPAddress, $p.Port) | |
| } | |
| } | |
| $deadline = (Get-Date).AddMilliseconds($TimeoutMs) | |
| while ((Get-Date) -lt $deadline) { | |
| $pending = $tasks | Where-Object { -not $_.Task.IsCompleted } | |
| if (-not $pending) { break } | |
| Start-Sleep -Milliseconds 30 | |
| } | |
| $open = foreach ($t in $tasks) { | |
| if ($t.Task.IsCompletedSuccessfully) { | |
| "$($t.Port)/$($t.Service)" | |
| } | |
| try { $t.Client.Close() } catch { } | |
| } | |
| return ($open -join ',') | |
| } | |
| #------------------------------------------------------------------------------- | |
| # mDNS / Bonjour: dns-sd.exe を使った機器名検索 | |
| #------------------------------------------------------------------------------- | |
| function Test-DnsSdAvailable { | |
| return ($null -ne (Get-Command dns-sd.exe -ErrorAction SilentlyContinue)) | |
| } | |
| function Resolve-MdnsHostname { | |
| param([string]$Hostname, [string]$DnsSdPath) | |
| if (-not $Hostname) { return $null } | |
| $Hostname = $Hostname.TrimEnd('.') | |
| # Method 1: System.Net.Dns (Windows 10+ の mDNS リゾルバ) | |
| try { | |
| $addrs = [System.Net.Dns]::GetHostAddresses($Hostname) | |
| $v4 = $addrs | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 | |
| if ($v4) { return $v4.ToString() } | |
| } catch { } | |
| # Method 2: dns-sd -G v4 で同期解決 | |
| if ($DnsSdPath) { | |
| $tempFile = [System.IO.Path]::GetTempFileName() | |
| try { | |
| $proc = Start-Process -FilePath $DnsSdPath ` | |
| -ArgumentList @('-G', 'v4', $Hostname) ` | |
| -RedirectStandardOutput $tempFile ` | |
| -NoNewWindow -PassThru -WindowStyle Hidden -ErrorAction Stop | |
| Start-Sleep -Milliseconds 1200 | |
| try { $proc | Stop-Process -Force -ErrorAction SilentlyContinue } catch { } | |
| $content = Get-Content $tempFile -Raw -ErrorAction SilentlyContinue | |
| if ($content -and $content -match '\s(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(\s|$)') { | |
| return $matches[1] | |
| } | |
| } catch { } | |
| finally { | |
| Remove-Item $tempFile -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| return $null | |
| } | |
| function Invoke-MdnsDiscovery { | |
| param( | |
| [int]$BrowseSeconds = 3, | |
| [int]$ResolveSeconds = 2 | |
| ) | |
| $cmd = Get-Command dns-sd.exe -ErrorAction SilentlyContinue | |
| if (-not $cmd) { | |
| Write-Host "[mDNS] dns-sd.exe が見つかりません (Bonjour for Windows 未インストール)。スキップ。" -ForegroundColor DarkYellow | |
| return @{} | |
| } | |
| $dnssdPath = $cmd.Source | |
| Write-Host ("[mDNS] dns-sd.exe で機器名を検索中 ({0}秒)..." -f $BrowseSeconds) -ForegroundColor Cyan | |
| $serviceTypes = @( | |
| '_http._tcp', '_https._tcp', '_ssh._tcp', | |
| '_ipp._tcp', '_ipps._tcp', '_pdl-datastream._tcp', '_printer._tcp', | |
| '_smb._tcp', '_afpovertcp._tcp', | |
| '_airplay._tcp', '_raop._tcp', '_googlecast._tcp', | |
| '_workstation._tcp', '_companion-link._tcp', | |
| '_apple-mobdev2._tcp', '_homekit._tcp', | |
| '_device-info._tcp' | |
| ) | |
| $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ('dns-sd-' + [Guid]::NewGuid().ToString('N')) | |
| New-Item -ItemType Directory -Path $tempDir -Force | Out-Null | |
| try { | |
| # ===== Stage 1: 各サービスタイプを並列 browse ===== | |
| $browseProcs = foreach ($svc in $serviceTypes) { | |
| $safeName = $svc -replace '[^a-zA-Z0-9]', '_' | |
| $outfile = Join-Path $tempDir "browse_$safeName.txt" | |
| $errfile = Join-Path $tempDir "browse_$safeName.err" | |
| try { | |
| $proc = Start-Process -FilePath $dnssdPath ` | |
| -ArgumentList @('-B', $svc, 'local.') ` | |
| -RedirectStandardOutput $outfile ` | |
| -RedirectStandardError $errfile ` | |
| -NoNewWindow -PassThru -WindowStyle Hidden -ErrorAction Stop | |
| [PSCustomObject]@{ Service = $svc; Process = $proc; OutFile = $outfile } | |
| } catch { | |
| $null | |
| } | |
| } | |
| $browseProcs = $browseProcs | Where-Object { $_ } | |
| Start-Sleep -Seconds $BrowseSeconds | |
| foreach ($p in $browseProcs) { | |
| try { $p.Process | Stop-Process -Force -ErrorAction SilentlyContinue } catch { } | |
| } | |
| # ===== Stage 2: 出力からインスタンス名を抽出 ===== | |
| $instances = @{} | |
| foreach ($p in $browseProcs) { | |
| if (-not (Test-Path $p.OutFile)) { continue } | |
| $lines = Get-Content $p.OutFile -ErrorAction SilentlyContinue | |
| foreach ($line in $lines) { | |
| if ($line -match '^\s*\d{1,2}:\d{2}:\d{2}\.\d+\s+Add\s+\d+\s+\d+\s+\S+\s+\S+\s+(.+?)\s*$') { | |
| $instance = $matches[1].Trim() | |
| if ($instance) { | |
| $key = "{0}|{1}" -f $p.Service, $instance | |
| if (-not $instances.ContainsKey($key)) { | |
| $instances[$key] = @{ Service = $p.Service; Instance = $instance } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if ($instances.Count -eq 0) { | |
| Write-Host "[mDNS] 応答なし (ネットワークに mDNS 機器がない or Bonjour サービス停止)。" -ForegroundColor DarkYellow | |
| return @{} | |
| } | |
| Write-Host ("[mDNS] {0} 件のサービス応答を発見。ホスト名解決中..." -f $instances.Count) -ForegroundColor Cyan | |
| # ===== Stage 3: 各インスタンスを -L で解決 ===== | |
| $resolveProcs = foreach ($key in $instances.Keys) { | |
| $info = $instances[$key] | |
| $safeName = $key -replace '[^a-zA-Z0-9]', '_' | |
| if ($safeName.Length -gt 80) { $safeName = $safeName.Substring(0, 80) } | |
| $outfile = Join-Path $tempDir "resolve_$safeName.txt" | |
| $errfile = Join-Path $tempDir "resolve_$safeName.err" | |
| try { | |
| $proc = Start-Process -FilePath $dnssdPath ` | |
| -ArgumentList @('-L', $info.Instance, $info.Service, 'local.') ` | |
| -RedirectStandardOutput $outfile ` | |
| -RedirectStandardError $errfile ` | |
| -NoNewWindow -PassThru -WindowStyle Hidden -ErrorAction Stop | |
| [PSCustomObject]@{ | |
| Instance = $info.Instance | |
| Service = $info.Service | |
| Process = $proc | |
| OutFile = $outfile | |
| } | |
| } catch { | |
| $null | |
| } | |
| } | |
| $resolveProcs = $resolveProcs | Where-Object { $_ } | |
| Start-Sleep -Seconds $ResolveSeconds | |
| foreach ($p in $resolveProcs) { | |
| try { $p.Process | Stop-Process -Force -ErrorAction SilentlyContinue } catch { } | |
| } | |
| # ===== Stage 4: 出力から target hostname を抽出 ===== | |
| # instance -> @{ Hostname; Services } | |
| $instanceMap = @{} | |
| foreach ($p in $resolveProcs) { | |
| if (-not (Test-Path $p.OutFile)) { continue } | |
| $content = Get-Content $p.OutFile -Raw -ErrorAction SilentlyContinue | |
| if (-not $content) { continue } | |
| if ($content -match 'can be reached at\s+(\S+?):\d+') { | |
| $hostname = $matches[1].TrimEnd('.') | |
| if (-not $instanceMap.ContainsKey($p.Instance)) { | |
| $instanceMap[$p.Instance] = @{ Hostname = $hostname; Services = New-Object System.Collections.Generic.List[string] } | |
| } | |
| if (-not $instanceMap[$p.Instance].Services.Contains($p.Service)) { | |
| $instanceMap[$p.Instance].Services.Add($p.Service) | |
| } | |
| } | |
| } | |
| # ===== Stage 5: hostname → IPv4 解決 ===== | |
| $ipMap = @{} | |
| foreach ($instance in $instanceMap.Keys) { | |
| $info = $instanceMap[$instance] | |
| $ip = Resolve-MdnsHostname -Hostname $info.Hostname -DnsSdPath $dnssdPath | |
| if (-not $ip) { continue } | |
| if (-not $ipMap.ContainsKey($ip)) { | |
| $ipMap[$ip] = [PSCustomObject]@{ | |
| Name = $instance | |
| Hostname = $info.Hostname | |
| Services = ($info.Services | Sort-Object) -join ',' | |
| } | |
| } else { | |
| # 同一IPに複数機能がぶら下がるケース (例: AirPlay + AirPort) | |
| if ($ipMap[$ip].Name -notmatch [regex]::Escape($instance)) { | |
| $ipMap[$ip].Name = "{0}; {1}" -f $ipMap[$ip].Name, $instance | |
| } | |
| $existing = $ipMap[$ip].Services -split ',' | |
| $combined = ($existing + $info.Services | Sort-Object -Unique | Where-Object { $_ }) -join ',' | |
| $ipMap[$ip].Services = $combined | |
| } | |
| } | |
| Write-Host ("[mDNS] {0} 機器の名前を取得。" -f $ipMap.Count) -ForegroundColor Green | |
| return $ipMap | |
| } | |
| finally { | |
| Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| #------------------------------------------------------------------------------- | |
| # 機器カテゴリ推定 (mDNS サービスも考慮) | |
| #------------------------------------------------------------------------------- | |
| function Get-DeviceTypeGuess { | |
| param( | |
| [string]$Vendor, | |
| [string]$OsGuess, | |
| [string]$OpenPorts, | |
| [string]$Hostname, | |
| [string]$NetBIOSName, | |
| [string]$MdnsName, | |
| [string]$MdnsServices, | |
| [string]$Note | |
| ) | |
| $v = $Vendor.ToLower() | |
| $h = "$Hostname $NetBIOSName $MdnsName".ToLower() | |
| $p = ",$OpenPorts," | |
| $m = ",$MdnsServices," | |
| if ($Note -match 'Gateway') { return 'Router / Default Gateway' } | |
| if ($v -match 'vmware|hyper-v|virtualbox|qemu|kvm|xen') { return "Virtual Machine ($Vendor)" } | |
| if ($v -match 'raspberry') { return 'Raspberry Pi' } | |
| if ($v -match 'espressif') { return 'ESP32 / ESP8266 / M5Stack (IoT)' } | |
| # mDNS シグナル優先 | |
| if ($m -match ',_googlecast\._tcp,') { return 'Chromecast / Google TV' } | |
| if ($m -match ',_airplay\._tcp,|,_raop\._tcp,') { return "AirPlay device ($Vendor)" } | |
| if ($m -match ',_homekit\._tcp,') { return "HomeKit device ($Vendor)" } | |
| if ($m -match ',_apple-mobdev2\._tcp,') { return 'Apple iOS device (iPhone/iPad)' } | |
| if ($m -match ',_companion-link\._tcp,' -and $v -match 'apple') { return 'Apple device (Mac/iPhone/iPad)' } | |
| if ($m -match ',_ipp\._tcp,|,_ipps\._tcp,|,_pdl-datastream\._tcp,|,_printer\._tcp,') { | |
| return "Printer ($Vendor)" | |
| } | |
| if ($m -match ',_workstation\._tcp,' -and $v -notmatch 'apple') { | |
| return "Linux/Unix workstation ($Vendor)" | |
| } | |
| # ポート系 | |
| if ($p -match ',9100/|,515/|,631/') { return "Printer ($Vendor)" } | |
| if ($v -match 'synology|qnap|netgear.*nas|buffalo' -and $p -match ',445/|,5000/|,548/') { | |
| return "NAS ($Vendor)" | |
| } | |
| if ($p -match ',8009/') { return 'Chromecast / Google Cast device' } | |
| if ($p -match ',7000/') { return "AirPlay device ($Vendor)" } | |
| if ($p -match ',32400/') { return "Plex Media Server ($Vendor)" } | |
| if ($v -match 'nintendo') { return 'Nintendo Switch / Wii' } | |
| if ($v -match 'sony.*interactive|playstation') { return 'PlayStation' } | |
| if ($v -match 'microsoft' -and $h -match 'xbox') { return 'Xbox' } | |
| if ($v -match 'apple') { | |
| if ($p -match ',548/|,7000/') { return 'Apple device (Mac/Apple TV)' } | |
| return 'Apple device (iPhone/iPad/Mac)' | |
| } | |
| if ($OsGuess -match 'Windows' -or $p -match ',445/|,3389/|,139/' -or $NetBIOSName) { | |
| if ($NetBIOSName) { return "Windows PC ($NetBIOSName)" } | |
| return 'Windows PC' | |
| } | |
| if ($OsGuess -match 'Linux' -and $p -match ',22/') { return "Linux/Unix host ($Vendor)" } | |
| if ($v -match 'buffalo|nec|netgear|tp-link|tplink|asus|aterm|cisco|aruba|i-?o data|iodata') { | |
| return "Router / AP ($Vendor)" | |
| } | |
| if ($v -match 'sony|sharp|panasonic|lg electronics|samsung') { | |
| return "Consumer electronics ($Vendor)" | |
| } | |
| if ($Vendor) { return "Unknown ($Vendor)" } | |
| return 'Unknown' | |
| } | |
| #------------------------------------------------------------------------------- | |
| # === メイン処理 === | |
| #------------------------------------------------------------------------------- | |
| Write-Host "===== LAN 機器識別スクリプト =====" -ForegroundColor Yellow | |
| # CSV 保存判定 | |
| $shouldSave = $SaveCsv -or $PSBoundParameters.ContainsKey('OutputPath') | |
| if ($shouldSave -and -not $OutputPath) { | |
| $OutputPath = Join-Path -Path (Get-Location) -ChildPath ("lan-identified_{0}.csv" -f (Get-Date -Format 'yyyyMMdd_HHmmss')) | |
| } | |
| # 入力取得 (InputCsv 指定が無ければ Discover が既定動作) | |
| if ($InputCsv) { | |
| if (-not (Test-Path $InputCsv)) { | |
| Write-Error "InputCsv が見つかりません: $InputCsv" | |
| exit 1 | |
| } | |
| $devices = Import-Csv -Path $InputCsv -Encoding UTF8 | |
| } else { | |
| Write-Host "[Discover] Get-NetNeighbor から取得中..." -ForegroundColor Cyan | |
| $devices = Get-NetNeighbor -AddressFamily IPv4 -ErrorAction SilentlyContinue | | |
| Where-Object { | |
| $_.State -in 'Reachable', 'Stale', 'Permanent' -and | |
| $_.LinkLayerAddress -and | |
| $_.LinkLayerAddress -ne '00-00-00-00-00-00' -and | |
| $_.IPAddress -notmatch '^(127\.|169\.254\.|22[4-9]\.|23[0-9]\.|255\.)' | |
| } | ForEach-Object { | |
| [PSCustomObject]@{ | |
| IPAddress = $_.IPAddress | |
| MACAddress = $_.LinkLayerAddress | |
| Hostname = '' | |
| State = $_.State | |
| Note = '' | |
| } | |
| } | |
| } | |
| if (-not $devices -or @($devices).Count -eq 0) { | |
| Write-Warning "対象機器がありません。" | |
| exit 0 | |
| } | |
| Write-Host ("[対象] {0} 台" -f @($devices).Count) -ForegroundColor Cyan | |
| if ($UseOnlineOuiLookup) { | |
| Write-Host "[Note] api.macvendors.com 利用 (1 req/sec 制限)" -ForegroundColor DarkYellow | |
| } | |
| # mDNS 検索 (一括) | |
| $mdnsMap = @{} | |
| if (-not $NoMdns) { | |
| if (Test-DnsSdAvailable) { | |
| $mdnsMap = Invoke-MdnsDiscovery -BrowseSeconds $MdnsBrowseSeconds | |
| } else { | |
| Write-Host "[mDNS] dns-sd.exe 未インストールのためスキップ (iTunes / Bonjour Print Services で導入可)。" -ForegroundColor DarkYellow | |
| } | |
| } | |
| # 各デバイスを enrich | |
| $results = foreach ($d in $devices) { | |
| Write-Host (" 処理中: {0,-15} {1}" -f $d.IPAddress, $d.MACAddress) -ForegroundColor DarkGray | |
| $vendor = Get-OuiVendor -MacAddress $d.MACAddress -UseOnline:$UseOnlineOuiLookup | |
| $ttlInfo = Get-PingTtl -IPAddress $d.IPAddress -TimeoutMs $PingTimeoutMs | |
| $nbName = Get-NetBIOSName -IPAddress $d.IPAddress | |
| $openPorts = '' | |
| if ($ScanPorts) { | |
| $openPorts = Test-OpenPorts -IPAddress $d.IPAddress -TimeoutMs $PortTimeoutMs | |
| } | |
| # mDNS 情報 | |
| $mdnsName = '' | |
| $mdnsHostname = '' | |
| $mdnsServices = '' | |
| if ($mdnsMap.ContainsKey($d.IPAddress)) { | |
| $info = $mdnsMap[$d.IPAddress] | |
| $mdnsName = $info.Name | |
| $mdnsHostname = $info.Hostname | |
| $mdnsServices = $info.Services | |
| } | |
| $deviceType = Get-DeviceTypeGuess ` | |
| -Vendor $vendor ` | |
| -OsGuess $ttlInfo.OsGuess ` | |
| -OpenPorts $openPorts ` | |
| -Hostname $d.Hostname ` | |
| -NetBIOSName $nbName ` | |
| -MdnsName $mdnsName ` | |
| -MdnsServices $mdnsServices ` | |
| -Note $d.Note | |
| [PSCustomObject]@{ | |
| IPAddress = $d.IPAddress | |
| MACAddress = $d.MACAddress | |
| Vendor = $vendor | |
| DeviceType = $deviceType | |
| MdnsName = $mdnsName | |
| MdnsHostname = $mdnsHostname | |
| MdnsServices = $mdnsServices | |
| OsGuess = $ttlInfo.OsGuess | |
| Ttl = $ttlInfo.Ttl | |
| Hostname = $d.Hostname | |
| NetBIOSName = $nbName | |
| OpenPorts = $openPorts | |
| State = $d.State | |
| Note = $d.Note | |
| } | |
| } | |
| # 表示 | |
| Write-Host ("`n===== 識別結果 =====") -ForegroundColor Green | |
| $results | Format-Table IPAddress, Vendor, DeviceType, MdnsName, NetBIOSName, OsGuess, OpenPorts -AutoSize -Wrap | |
| # CSV 保存 (明示指定時のみ) | |
| if ($shouldSave) { | |
| $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 | |
| $saveMsg = "`nCSV を保存しました: {0}" -f $OutputPath | |
| Write-Host $saveMsg -ForegroundColor Cyan | |
| } else { | |
| Write-Host "`n(CSV保存はスキップ。保存するには -SaveCsv または -OutputPath を指定)" -ForegroundColor DarkGray | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment