Private/Kinds/NetworkDependencyReference.ps1
|
# What on this machine still points at a Server that does not answer: a part of the # NetworkDependency Kind, and no Kind. # # Windows logs that something asked for a Server whose name does not resolve - 339 times # in a month, on one machine, only in working hours and at no regular interval. That is # somebody working: every time the Explorer shows its quick access, a program opens its # list of recent files or a dialog shows a remembered place, Windows tries the Server # again and waits for it to fail. The Finding about failed connections said "look for # what still points at it", and a Technician had nowhere to start. Observed 2026-10-14: # no drive, no printer, no autostart entry and no scheduled task named that Server; what # did was in none of the places a Run looked in. # # So this looks, for the Servers Windows logged as failed and for no others, in the # places of the User of the Run where such a pointer lives: # # quick-access the Explorer's quick access: its pinned and its frequent folders # jump-list the lists of recent files that programs keep in the taskbar # recent the shortcuts Windows writes for every file that was opened # shortcut shortcuts on the desktop, in the links and in the start menu # place network places, and the shares the Explorer remembers visiting # office paths in the settings of Office: templates, default folders, recent files # typed addresses typed into the Explorer and into the Run dialog # drive drives that are to be reconnected at sign-in # printer printer connections of the User # printer-port the TCP/IP ports of the printers this machine knows, by the host they # print to: a printer shared from a print Server can print through a port # that points at another Server, and its connection names only the first. # Observed 2026-10-14: the address that failed was such a port's. # # Kept is the Server, the place and the share - "\\Server\Share" - and never the path # below it or the name of a file: which documents somebody opened is what that person # did, and a Run reads the state of a machine (ADR-0006). The network is the exception # that ADR makes, and a share is the network. # # Only for the User of the Run: these places are in that User's profile and registry. # How large a file may be to be searched, and how many files of one place. $script:ReferenceMaxFileBytes = 4MB $script:ReferenceMaxFiles = 1500 # How many values of the registry are read under one key at most. $script:ReferenceMaxValues = 40000 # How many Servers are looked for: the ones most often asked for in vain. $script:ReferenceMaxServers = 8 function Get-ReferencePattern { <# .SYNOPSIS What a pointer at one of several Servers looks like in a text: "\\Server\Share", the Server with or without its domain, in any case. One pattern for all of them, so that a text is read once however many Servers are looked for. Pure. #> [CmdletBinding()] [OutputType([regex])] param([Parameter(Mandatory)][string[]]$Server) # A Server looked for by its FQDN is pointed at by its short name as well: \\fs01\Drucker # points at fs01.contoso.example, which Find-ReferenceShare decides by Test-SameServer. $names = @($Server | ForEach-Object { "$_".Trim() } | Where-Object { $_ } | ForEach-Object { $_ if ($_.Contains('.') -and -not (Test-NetworkAddress -Text $_)) { ($_ -split '\.')[0] } } | Select-Object -Unique | ForEach-Object { [regex]::Escape($_) }) -join '|' # In a registry key and in a printer connection the separators are hashes or commas # instead of backslashes: "##Server#Share" and ",,Server,Printer". The share ends where # the path goes on, the text ends or a character no path holds stands, and holds none # of 0x80-0xBF: read as bytes, what follows a path in a binary file is noise, and # "\\FILER\pu" followed by it was a share that is not there. [regex]('(?i)(?:\\\\|,,|##)(?<server>(?:' + $names + ')(?:\.[a-z0-9.-]+)?)(?:\\|,|#)(?<share>[^\\,#\x00-\x1f\x80-\xbf"<>|?*:]{1,80})(?=[\x00-\x1f\\,#"<>|?*:]|$)') } function Find-ReferenceShare { <# .SYNOPSIS The shares of the Servers looked for that a text points at: each as its Server, written as it was asked for, and "\\Server\Share", each once. Nothing where it points at none. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Text, [Parameter(Mandatory)][string[]]$Server, [regex]$Pattern) if (-not $Text) { return } if (-not $Pattern) { $Pattern = Get-ReferencePattern -Server $Server } $names = @($Server | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($match in $Pattern.Matches($Text)) { $share = $match.Groups['share'].Value.Trim() if (-not $share) { continue } # Which Server looked for the host is, by the one rule of what is one Server: a # host written with a domain the Server was not looked for with is another one. $written = $match.Groups['server'].Value $asked = @($names | Where-Object { $_ -eq $written })[0] if (-not $asked) { $asked = @($names | Where-Object { Test-SameServer -Name $written -Other $_ })[0] } if (-not $asked) { continue } $unc = '\\{0}\{1}' -f $asked, $share if ($seen.Add($unc)) { [pscustomobject]@{ Server = $asked; Share = $unc } } } } function Read-ReferenceFileText { <# .SYNOPSIS What a file holds as text, whatever kind of file it is: read as the two ways Windows writes a path into a shortcut or a list. Empty where it would not open. .DESCRIPTION A shortcut and a jump list are binary files that hold their paths as plain characters, one byte each or two. Read as both, and as two bytes from either start, because such a path begins wherever the file put it. #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)][string]$Path) try { $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, ([IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete)) try { $length = [int][math]::Min([long]$script:ReferenceMaxFileBytes, $stream.Length) $bytes = New-Object byte[] $length $read = $stream.Read($bytes, 0, $length) } finally { $stream.Dispose() } if ($read -lt 2) { return '' } # The Explorer's lists are compound files: their links lie in pieces of 64 bytes, # scattered, and read flat a path came out cut or in garbage. Read as what they are, # each link's network path; read flat only where the file is no compound file. if (Test-CompoundFile -Bytes $bytes -Length $read) { $links = @(Read-CompoundFileStream -Bytes $bytes -Length $read | ForEach-Object { Get-LinkNetworkPath -Bytes $_.Bytes } | Where-Object { $_ }) return ($links -join "`n") } $latin = [Text.Encoding]::GetEncoding(28591).GetString($bytes, 0, $read) $wide = [Text.Encoding]::Unicode.GetString($bytes, 0, $read - ($read % 2)) $odd = [Text.Encoding]::Unicode.GetString($bytes, 1, ($read - 1) - (($read - 1) % 2)) "$latin`n$wide`n$odd" } catch { '' } } # A compound file's signature, and what its tables say of a sector that is no data. $script:CompoundSignature = [byte[]](0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1) $script:CompoundEndOfChain = [uint32]4294967294 # 0xFFFFFFFE: PowerShell reads that literal as -2 $script:CompoundFree = [uint32]4294967295 # 0xFFFFFFFF function Test-CompoundFile { # Whether bytes begin as a compound file does. Pure. [CmdletBinding()] [OutputType([bool])] param([AllowNull()][byte[]]$Bytes, [int]$Length = -1) if ($Length -lt 0 -and $Bytes) { $Length = $Bytes.Length } if (-not $Bytes -or $Length -lt 512) { return $false } for ($i = 0; $i -lt 8; $i++) { if ($Bytes[$i] -ne $script:CompoundSignature[$i]) { return $false } } $true } function Read-CompoundFileStream { <# .SYNOPSIS The streams of a compound file whose names are numbers - the links of a jump list - each as its name and its bytes. Nothing where the file is no compound file. Pure. .DESCRIPTION A compound file (MS-CFB) is a file system in a file: sectors of 512 or 4096 bytes, a table of which follows which (the FAT, found through the DIFAT), a directory of streams, and the small streams in 64-byte pieces of a mini stream with a table of their own. A jump list keeps each link as a small stream named by a number, and a list of them, DestList, which is left out. Every chain is walked at most as many steps as there are sectors, so a damaged file ends the reading instead of going round for ever; what cannot be read is left out. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][byte[]]$Bytes, [int]$Length = -1) if ($Length -lt 0 -and $Bytes) { $Length = $Bytes.Length } if (-not (Test-CompoundFile -Bytes $Bytes -Length $Length)) { return } $u16 = { param([int]$At) if ($At -lt 0 -or $At + 2 -gt $Length) { return 0 }; [BitConverter]::ToUInt16($Bytes, $At) } $u32 = { param([long]$At) if ($At -lt 0 -or $At + 4 -gt $Length) { return $script:CompoundFree }; [BitConverter]::ToUInt32($Bytes, [int]$At) } $sectorSize = [int]([math]::Pow(2, (& $u16 0x1E))) $miniSize = [int]([math]::Pow(2, (& $u16 0x20))) if ($sectorSize -notin 512, 4096 -or $miniSize -ne 64) { return } $sectors = [int][math]::Floor(($Length - $sectorSize) / $sectorSize) $offsetOf = { param([uint32]$Sector) [long]($Sector + 1) * $sectorSize } # The FAT: its sectors as the header's DIFAT names them, and the DIFAT sectors after. $fatSectors = New-Object System.Collections.Generic.List[uint32] for ($i = 0; $i -lt 109; $i++) { $s = & $u32 (0x4C + 4 * $i); if ($s -lt $script:CompoundEndOfChain) { $fatSectors.Add($s) } } $difat = & $u32 0x44 $per = $sectorSize / 4 - 1 for ($step = 0; $difat -lt $script:CompoundEndOfChain -and $step -le $sectors; $step++) { $at = & $offsetOf $difat for ($i = 0; $i -lt $per; $i++) { $s = & $u32 ($at + 4 * $i); if ($s -lt $script:CompoundEndOfChain) { $fatSectors.Add($s) } } $difat = & $u32 ($at + 4 * $per) } # Read straight, and not through a script block per entry: a list of a thousand links # has a FAT of tens of thousands of entries. $fat = New-Object System.Collections.Generic.List[uint32] foreach ($s in $fatSectors) { $at = [int](& $offsetOf $s) if ($at + $sectorSize -gt $Length) { continue } for ($i = 0; $i -lt $sectorSize; $i += 4) { $fat.Add([BitConverter]::ToUInt32($Bytes, $at + $i)) } } # A chain of pieces as the bytes they hold, cut to a size where one is given: the places # of its pieces first, then one copy each. $gather = { # Base: 1 where the pieces are the file's sectors, the header standing before the # first; 0 in the mini stream. param([byte[]]$From, [int]$FromLength, [int]$Piece, $Next, [uint32]$Start, [long]$Size, [int]$Base) $places = New-Object System.Collections.Generic.List[int] $s = $Start $most = [int][math]::Floor($FromLength / $Piece) + 1 while ($s -lt $script:CompoundEndOfChain -and $s -lt $Next.Count -and $places.Count -le $most) { $at = ([int]$s + $Base) * $Piece if ($at + $Piece -gt $FromLength) { break } $places.Add($at) if ($Size -ge 0 -and $places.Count * $Piece -ge $Size) { break } $s = $Next[[int]$s] } $total = $places.Count * $Piece if ($Size -ge 0 -and $total -gt $Size) { $total = [int]$Size } $out = New-Object byte[] $total for ($k = 0; $k -lt $places.Count; $k++) { $take = [math]::Min($Piece, $total - $k * $Piece) if ($take -le 0) { break } [Buffer]::BlockCopy($From, $places[$k], $out, $k * $Piece, $take) } , $out } $chain = { param([uint32]$Start, [long]$Size = -1) & $gather $Bytes $Length $sectorSize $fat $Start $Size 1 } $directory = & $chain (& $u32 0x30) if ($directory.Length -lt 128) { return } $cutoff = & $u32 0x38 $entries = @(for ($i = 0; $i + 128 -le $directory.Length; $i += 128) { $nameLength = [BitConverter]::ToUInt16($directory, $i + 0x40) [pscustomobject]@{ Name = $(if ($nameLength -ge 2 -and $nameLength -le 64) { [Text.Encoding]::Unicode.GetString($directory, $i, $nameLength - 2) } else { '' }) Type = $directory[$i + 0x42] Start = [BitConverter]::ToUInt32($directory, $i + 0x74) Size = [long][BitConverter]::ToUInt32($directory, $i + 0x78) } }) $rootEntry = @($entries | Where-Object { $_.Type -eq 5 })[0] if (-not $rootEntry) { return } # The small streams: in the mini stream, the root's chain, by a table of their own. $mini = & $chain $rootEntry.Start $rootEntry.Size $miniTable = @() $miniCount = & $u32 0x40 if ($miniCount -gt 0 -and $miniCount -lt $script:CompoundEndOfChain) { $table = & $chain (& $u32 0x3C) $miniTable = @(for ($i = 0; $i + 4 -le $table.Length; $i += 4) { [BitConverter]::ToUInt32($table, $i) }) } foreach ($entry in @($entries | Where-Object { $_.Type -eq 2 -and $_.Name -match '^[0-9a-fA-F]+$' -and $_.Size -gt 0 })) { if ($entry.Size -ge $cutoff) { $data = & $chain $entry.Start $entry.Size } else { $data = & $gather $mini $mini.Length $miniSize $miniTable $entry.Start $entry.Size 0 } if ($data.Length) { [pscustomobject]@{ Name = $entry.Name; Bytes = [byte[]]$data } } } } function Get-LinkNetworkPath { <# .SYNOPSIS The network path a Shell Link (.lnk) points at - "\\Server\Share\rest" - or nothing where it points at none. Pure. .DESCRIPTION By the link's own layout (MS-SHLLINK): past the header and the list of item ids, the LinkInfo, whose CommonNetworkRelativeLink names the share (NetName) and whose CommonPathSuffix the rest of the path. In Unicode where the link holds it so, else in the code page it was written in, read as Latin-1. A link that would not read as one is nothing. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][byte[]]$Bytes) if (-not $Bytes -or $Bytes.Length -lt 0x4C -or [BitConverter]::ToUInt32($Bytes, 0) -ne 0x4C) { return } $length = $Bytes.Length $u16 = { param([int]$At) if ($At -lt 0 -or $At + 2 -gt $length) { return -1 }; [int][BitConverter]::ToUInt16($Bytes, $At) } $u32 = { param([int]$At) if ($At -lt 0 -or $At + 4 -gt $length) { return -1 }; [long][BitConverter]::ToUInt32($Bytes, $At) } $ansi = { param([long]$At) if ($At -lt 0 -or $At -ge $length) { return '' } $end = [Array]::IndexOf($Bytes, [byte]0, [int]$At) if ($end -lt 0) { $end = $length } [Text.Encoding]::GetEncoding(28591).GetString($Bytes, [int]$At, $end - [int]$At) } $wide = { param([long]$At) if ($At -lt 0 -or $At -ge $length) { return '' } $end = [int]$At while ($end + 1 -lt $length -and -not ($Bytes[$end] -eq 0 -and $Bytes[$end + 1] -eq 0)) { $end += 2 } [Text.Encoding]::Unicode.GetString($Bytes, [int]$At, $end - [int]$At) } $flags = & $u32 0x14 if ($flags -lt 0 -or -not ($flags -band 0x2)) { return } $at = 0x4C if ($flags -band 0x1) { $idList = & $u16 $at; if ($idList -lt 0) { return }; $at += 2 + $idList } $infoSize = & $u32 $at $headerSize = & $u32 ($at + 4) $infoFlags = & $u32 ($at + 8) if ($infoSize -lt 0x1C -or $at + $infoSize -gt $length -or -not ($infoFlags -band 0x2)) { return } $network = $at + (& $u32 ($at + 20)) $suffix = $(if ($headerSize -ge 0x24) { & $wide ($at + (& $u32 ($at + 32))) } else { & $ansi ($at + (& $u32 ($at + 24))) }) $netOffset = & $u32 ($network + 8) $name = $(if ($netOffset -gt 0x14) { & $wide ($network + (& $u32 ($network + 20))) } else { & $ansi ($network + $netOffset) }) if (-not $name -or -not $name.StartsWith('\\')) { return } if ($suffix) { return ('{0}\{1}' -f $name.TrimEnd('\'), $suffix) } $name } function Get-ReferencePlace { <# .SYNOPSIS The places of the User of the Run in which a pointer at a Server lives: each as its token and either the files or the registry key to search. Asks only. #> [CmdletBinding()] [OutputType([psobject])] param() $roaming = [Environment]::GetFolderPath('ApplicationData') $recent = Join-Path $roaming 'Microsoft\Windows\Recent' $file = { param($Token, $Folder, $Filter, [switch]$Deep) if (-not (Test-Path -LiteralPath $Folder -PathType Container)) { return } $found = @(Get-ChildItem -LiteralPath $Folder -Filter $Filter -File -Force -Recurse:$Deep -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First $script:ReferenceMaxFiles | ForEach-Object { $_.FullName }) if ($found.Count) { [pscustomobject]@{ Token = $Token; Files = $found; Key = '' } } } # The Explorer's own two lists are known by their names; every other is a program's. $own = 'f01b4d95cf55d32a.automaticDestinations-ms', '5f7b5f1e01b83767.automaticDestinations-ms' $lists = Join-Path $recent 'AutomaticDestinations' if (Test-Path -LiteralPath $lists -PathType Container) { $all = @(Get-ChildItem -LiteralPath $lists -File -Force -ErrorAction SilentlyContinue) $quick = @($all | Where-Object { $own -contains $_.Name } | ForEach-Object { $_.FullName }) $other = @($all | Where-Object { $own -notcontains $_.Name } | Select-Object -First $script:ReferenceMaxFiles | ForEach-Object { $_.FullName }) if ($quick.Count) { [pscustomobject]@{ Token = 'quick-access'; Files = $quick; Key = '' } } if ($other.Count) { [pscustomobject]@{ Token = 'jump-list'; Files = $other; Key = '' } } } & $file 'jump-list' (Join-Path $recent 'CustomDestinations') '*' & $file 'recent' $recent '*.lnk' & $file 'shortcut' ([Environment]::GetFolderPath('DesktopDirectory')) '*.lnk' & $file 'shortcut' (Join-Path ([Environment]::GetFolderPath('UserProfile')) 'Links') '*.lnk' & $file 'shortcut' ([Environment]::GetFolderPath('StartMenu')) '*.lnk' -Deep & $file 'place' (Join-Path $roaming 'Microsoft\Windows\Network Shortcuts') '*' -Deep foreach ($pair in @( @('place', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2'), @('office', 'HKCU:\Software\Microsoft\Office'), @('typed', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths'), @('typed', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU'), @('drive', 'HKCU:\Network'), @('printer', 'HKCU:\Printers\Connections'))) { if (Test-Path -LiteralPath $pair[1]) { [pscustomobject]@{ Token = $pair[0]; Files = @(); Key = $pair[1] } } } } function Read-ReferenceRegistryText { <# .SYNOPSIS Everything under one registry key that could hold a path, as one text: the names of its keys and the names and texts of its values. Asks only. #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)][string]$Key) $text = New-Object System.Text.StringBuilder $left = $script:ReferenceMaxValues $keys = @(Get-Item -LiteralPath $Key -ErrorAction SilentlyContinue) + @(Get-ChildItem -LiteralPath $Key -Recurse -ErrorAction SilentlyContinue) foreach ($one in $keys) { if ($left -le 0) { break } $null = $text.AppendLine($one.PSChildName) foreach ($name in $one.GetValueNames()) { if (--$left -le 0) { break } $value = $one.GetValue($name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) if ($value -is [string]) { $null = $text.Append($name).Append('=').AppendLine($value) } elseif ($value -is [string[]]) { $null = $text.Append($name).Append('=').AppendLine(($value -join "`n")) } elseif ($value -is [byte[]] -and $value.Length -le 65536) { # A list of recent places is kept as bytes that hold the path two bytes a letter. $null = $text.Append($name).Append('=').AppendLine([Text.Encoding]::Unicode.GetString($value, 0, $value.Length - ($value.Length % 2))) } else { $null = $text.AppendLine($name) } } } $text.ToString() } function Find-ReferencePrinterPort { <# .SYNOPSIS The printers whose TCP/IP port points at one of the Servers looked for: each as its Server, written as it was asked for, and "Printer (Port)". The host of a port and a Server are one by Test-SameServer: an address is the same address, and nothing is looked up. Pure. .PARAMETER Printer { Name, PortName } per printer, as Win32_Printer has them. .PARAMETER Port { Name, HostAddress } per TCP/IP port, as Win32_TCPIPPrinterPort has them. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Printer, [AllowNull()][AllowEmptyCollection()]$Port, [Parameter(Mandatory)][string[]]$Server) $hostOf = @{} foreach ($one in @($Port | Where-Object { $_ })) { $hostOf["$(Get-DataProperty $one 'Name')"] = "$(Get-DataProperty $one 'HostAddress')".Trim() } foreach ($one in @($Printer | Where-Object { $_ })) { $portName = "$(Get-DataProperty $one 'PortName')" $target = $hostOf[$portName] if (-not $target) { continue } $asked = @($Server | Where-Object { Test-SameServer -Name $target -Other $_ })[0] if ($asked) { [pscustomobject]@{ Server = $asked; Share = '{0} ({1})' -f "$(Get-DataProperty $one 'Name')", $portName } } } } function Get-ReferenceFailedServer { <# .SYNOPSIS The Servers Windows logged as asked for in vain, the most often asked for first: from what the Check of the network's disruptions gathered. Pure. #> [CmdletBinding()] [OutputType([string[]])] param([AllowNull()]$NetworkDisruption, [string[]]$Kind = @('smb-connect-failed'), [int]$First = $script:ReferenceMaxServers) # fs01 and fs01.contoso.example are one Server, asked for in vain under both names: one # to look for, and not a second that "no pointer was found at". $names = @((Get-DataCollection $NetworkDisruption 'Disruptions') | Where-Object { $_ -and $Kind -contains "$(Get-DataProperty $_ 'Kind')" } | ForEach-Object { ConvertTo-ServerName -Name (Get-DataProperty $_ 'Server') } | Where-Object { $_ }) # Each name keyed once, and counted as often as it was asked for: one per event. $unique = @($names | Select-Object -Unique) $keyOf = @{} foreach ($name in $unique) { $keyOf[$name] = Get-ServerKey -Name $name -Among $unique } @($names | ForEach-Object { $keyOf[$_] } | Group-Object | Sort-Object @{ Expression = 'Count'; Descending = $true }, Name | Select-Object -First $First | ForEach-Object { $_.Name }) } function Select-ReferenceUnanswered { <# .SYNOPSIS Of the Servers asked for in vain, those that did not answer in this Run: a Server whose port this Check measured open is no Server that does not answer, though it failed in the days before. Pure. .DESCRIPTION Observed: fs01 was down for an afternoon, Windows logged the printers it could not reach, and the Report said "remove the printer connection to the old Server" of a Server that answered on 445 in the same Run and held the mapped drives. What still points at a Server that answers is no stale pointer. .PARAMETER Server The Servers looked for. .PARAMETER Measured The Servers this Check measured, as Get-NetworkDependencyData returns them. #> [CmdletBinding()] [OutputType([string[]])] param([AllowNull()][AllowEmptyCollection()][string[]]$Server, [AllowNull()][AllowEmptyCollection()]$Measured) $answered = @(@($Measured) | Where-Object { $_ -and (Get-DataProperty $_ 'Reachable') -eq $true } | ForEach-Object { "$(Get-DataProperty $_ 'Server')" } | Where-Object { $_ }) @(@($Server) | Where-Object { $name = $_; $name -and -not @($answered | Where-Object { Test-SameServer -Name $name -Other $_ }).Count }) } function Get-NetworkDependencyReference { <# .SYNOPSIS Where on this machine the Servers that were asked for in vain are still pointed at. Returns what was searched for, where, and what was found. Asks only. .DESCRIPTION Nothing is searched where no Server failed, or nobody read whether one did: the places are then not opened at all. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$NetworkDisruption) $servers = @(Get-ReferenceFailedServer -NetworkDisruption $NetworkDisruption) if (-not $servers.Count) { return [pscustomobject]@{ Searched = $false; Servers = @(); Places = @(); Found = @(); PortsUnread = $false } } # A place may be several folders - the shortcuts are the desktop's and the start # menu's - and is one place in what is said: counted and named together. $pattern = Get-ReferencePattern -Server $servers $places = New-Object 'System.Collections.Generic.List[string]' $rows = [ordered]@{} foreach ($place in @(Get-ReferencePlace)) { if (-not $places.Contains($place.Token)) { $places.Add($place.Token) } $texts = @() if ($place.Key) { $texts = @(Read-ReferenceRegistryText -Key $place.Key) } else { $texts = @($place.Files | ForEach-Object { Read-ReferenceFileText -Path $_ }) } foreach ($text in $texts) { $in = @(Find-ReferenceShare -Text $text -Server $servers -Pattern $pattern) foreach ($server in @($in | ForEach-Object { $_.Server } | Select-Object -Unique)) { $key = '{0}|{1}' -f $server, $place.Token if (-not $rows.Contains($key)) { $rows[$key] = [pscustomobject]@{ Server = $server; Place = $place.Token; Count = 0 Shares = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) } } $rows[$key].Count++ foreach ($one in @($in | Where-Object { $_.Server -eq $server })) { $null = $rows[$key].Shares.Add($one.Share) } } } } # The printers' ports, read as the Printing Kind reads them, without admin rights. # Where they cannot be read, that is said: the search did not go there. $portsUnread = $false $printerRows = @() try { $ports = @(Get-CimInstance Win32_TCPIPPrinterPort -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)"; HostAddress = "$($_.HostAddress)" } }) $printers = @(Get-CimInstance Win32_Printer -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)"; PortName = "$($_.PortName)" } }) $printerRows = @(Find-ReferencePrinterPort -Printer $printers -Port $ports -Server $servers) $places.Add('printer-port') } catch { $portsUnread = $true } foreach ($server in @($printerRows | ForEach-Object { $_.Server } | Select-Object -Unique)) { $rows['{0}|printer-port' -f $server] = [pscustomobject]@{ Server = $server; Place = 'printer-port' Count = @($printerRows | Where-Object { $_.Server -eq $server }).Count Shares = New-Object 'System.Collections.Generic.HashSet[string]' ([string[]]@($printerRows | Where-Object { $_.Server -eq $server } | ForEach-Object { $_.Share }), [StringComparer]::OrdinalIgnoreCase) } } # In the order the Servers were asked for, and within one in the order of the places. $found = @(foreach ($server in $servers) { foreach ($row in @($rows.Values | Where-Object { $_.Server -eq $server })) { [pscustomobject]@{ Server = $row.Server; Place = $row.Place; Count = $row.Count; Shares = @($row.Shares | Sort-Object) } } }) [pscustomobject]@{ Searched = $true; Servers = $servers; Places = @($places); Found = @($found); PortsUnread = $portsUnread } } function Get-ReferencePlaceText { # A place as the Report names it. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()][string]$Place) switch ("$Place") { 'quick-access' { return (Get-Text 'Value.Reference.Place.QuickAccess') } 'jump-list' { return (Get-Text 'Value.Reference.Place.JumpList') } 'recent' { return (Get-Text 'Value.Reference.Place.Recent') } 'shortcut' { return (Get-Text 'Value.Reference.Place.Shortcut') } 'place' { return (Get-Text 'Value.Reference.Place.Place') } 'office' { return (Get-Text 'Value.Reference.Place.Office') } 'typed' { return (Get-Text 'Value.Reference.Place.Typed') } 'drive' { return (Get-Text 'Value.Reference.Place.Drive') } 'printer' { return (Get-Text 'Value.Reference.Place.Printer') } 'printer-port' { return (Get-Text 'Value.Reference.Place.PrinterPort') } } "$Place" } function Get-ReferencePlaceHint { # What to do about a pointer in a place. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()][string]$Place) switch ("$Place") { 'quick-access' { return (Get-Text 'Hint.Reference.QuickAccess') } 'jump-list' { return (Get-Text 'Hint.Reference.Recent') } 'recent' { return (Get-Text 'Hint.Reference.Recent') } 'shortcut' { return (Get-Text 'Hint.Reference.Shortcut') } 'place' { return (Get-Text 'Hint.Reference.Place') } 'office' { return (Get-Text 'Hint.Reference.Office') } 'typed' { return (Get-Text 'Hint.Reference.Typed') } 'drive' { return (Get-Text 'Hint.Reference.Drive') } 'printer' { return (Get-Text 'Hint.Reference.Printer') } 'printer-port' { return (Get-Text 'Hint.Reference.PrinterPort') } } '' } function New-NetworkDependencyReferenceFinding { <# .SYNOPSIS Where this machine still points at the Servers it asked for in vain, and what to do about each place. Nothing where nothing was searched. .DESCRIPTION A WARN where a pointer was found: it is the thing to remove, and the answer to the Finding about failed connections. Where none was found that is said too, with where the search went - so that the Technician knows what is left to look at, and does not look there again. #> [CmdletBinding()] param([AllowNull()]$Data) $search = Get-DataProperty $Data 'References' if (-not [bool](Get-DataProperty $search 'Searched')) { return } $servers = @(Select-ReferenceUnanswered -Server @((Get-DataCollection $search 'Servers') | Where-Object { $_ }) ` -Measured (Get-DataCollection $Data 'Servers')) if (-not $servers.Count) { return } $check = Get-Text 'Check.Reference.Pointers' $found = @((Get-DataCollection $search 'Found') | Where-Object { $_ -and $servers -contains "$(Get-DataProperty $_ 'Server')" }) $places = @((Get-DataCollection $search 'Places') | Where-Object { $_ }) $looked = (Get-Text 'Value.Reference.Looked') -f (@($places | ForEach-Object { Get-ReferencePlaceText -Place $_ }) -join '; ') if ([bool](Get-DataProperty $search 'PortsUnread')) { $looked = '{0} | {1}' -f $looked, (Get-Text 'Value.Reference.PortsUnread') } $reference = @(New-Reference -Section 'Title.Reference.Pointers') if (-not $found.Count) { return New-Finding -Category Network -Check $check -Severity INFO ` -Value ('{0} | {1}' -f ((Get-Text 'Value.Reference.None') -f ($servers -join ', ')), $looked) ` -Meaning (Get-Text 'Meaning.Reference.None') -Hint (Get-Text 'Hint.Reference.None') } $value = @() foreach ($server in $servers) { $mine = @($found | Where-Object { "$(Get-DataProperty $_ 'Server')" -eq $server }) if (-not $mine.Count) { continue } $items = @($mine | ForEach-Object { $shares = @((Get-DataCollection $_ 'Shares') | Where-Object { $_ }) $shown = @($shares | Select-Object -First 4) -join ', ' if ($shares.Count -gt 4) { $shown = (Get-Text 'Value.Reference.MoreShares') -f $shown, ($shares.Count - 4) } (Get-Text 'Value.Reference.Item') -f (Get-ReferencePlaceText -Place "$(Get-DataProperty $_ 'Place')"), $shown }) $value += '{0}: {1}' -f $server, ($items -join '; ') } $without = @($servers | Where-Object { $name = $_; -not @($found | Where-Object { "$(Get-DataProperty $_ 'Server')" -eq $name }).Count }) if ($without.Count) { $value += (Get-Text 'Value.Reference.NoneFor') -f ($without -join ', ') } # A hint for each place that holds one, each once, in the order of the places. $hints = @($found | ForEach-Object { "$(Get-DataProperty $_ 'Place')" } | Select-Object -Unique | ForEach-Object { Get-ReferencePlaceHint -Place $_ } | Where-Object { $_ } | Select-Object -Unique) $hints += Get-Text 'Hint.Reference.After' New-Finding -Category Network -Check $check -Severity WARN -Value ($value -join ' | ') ` -Meaning (Get-Text 'Meaning.Reference.Found') -Hint ($hints -join ' | ') -Reference $reference } function ConvertTo-NetworkDependencyReferenceSection { <# .SYNOPSIS Every place that points at a Server that was asked for in vain: the Server, the place, in how many files or keys, and the shares. Nothing where none does. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $search = Get-DataProperty $Data 'References' $servers = @(Select-ReferenceUnanswered -Server @((Get-DataCollection $search 'Servers') | Where-Object { $_ }) ` -Measured (Get-DataCollection $Data 'Servers')) $found = @((Get-DataCollection $search 'Found') | Where-Object { $_ -and $servers -contains "$(Get-DataProperty $_ 'Server')" }) if (-not $found.Count) { return } New-Section -Title (Get-Text 'Title.Reference.Pointers') -Help (Get-Text 'Help.Reference.Pointers') -Row @( $found | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Reference.Server')] = "$(Get-DataProperty $_ 'Server')" $row[(Get-Text 'Column.Reference.Place')] = Get-ReferencePlaceText -Place "$(Get-DataProperty $_ 'Place')" $row[(Get-Text 'Column.Reference.Count')] = [int](ConvertTo-Number (Get-DataProperty $_ 'Count')) $row[(Get-Text 'Column.Reference.Shares')] = @((Get-DataCollection $_ 'Shares') | Where-Object { $_ }) -join ', ' [pscustomobject]$row } ) } |