Private/Lead.ps1
|
# Leads: what several Findings point to together. # # A blue screen from a read error, eleven disk errors and a drive that reports wear are # three Findings in three places, and seeing that they are one story is the step that # takes experience. Once every Check has run, a Run reads its Findings together and says # what several of them point to. A Lead is not a Finding: it observes nothing, has no # Severity and is not counted. See ADR-0005. # # A rule is a name and the Signals that support it. It goes by Signals and never by what # a Finding says, so that rewording a Finding cannot stop a Lead from forming. $script:LeadRules = @( [pscustomobject]@{ Name = 'drive'; Signals = @('disk-errors', 'drive-health-warning', 'stop-code-disk', 'disk-write-slow', 'program-not-paged-in') } # Of the hardware errors Windows logs, only those of the memory: a processor or a bus # error beside a memory stop code is no second sign of the memory. [pscustomobject]@{ Name = 'memory'; Signals = @('stop-code-memory', 'memory-errors', 'access-violations-spread') } [pscustomobject]@{ Name = 'graphics'; Signals = @('live-kernel-graphics', 'stop-code-graphics', 'graphics-driver-old') } # Several Servers affected is this machine's connection; one Server affected is that # Server. The two go by different Signals, so that neither is taken for the other. # # Background is a Signal that is how most machines are and no observation of anything # going wrong: power saving on the adapter is what Windows sets. It supports a Lead # beside something that was observed, from a Finding that is in order too - and by # itself it starts nothing, not even "no Lead possible": that stood in every Report # of a Run that left Checks out, about a machine with nothing wrong. [pscustomobject]@{ Name = 'connection'; Signals = @('link-dropped', 'servers-lost', 'cable-wifi-switching', 'loss-on-own-network', 'usb-network-adapter', 'adapter-power-saving', 'crash-with-network-disruption') Background = @('adapter-power-saving') } # One Lead for each Server, or for one of its Services: its Findings are read together # by what they are about. A Server that answered apart from what fails on it - its ping, # a Service nothing failed on - is up, and what fails is read Service by Service: one # service, a firewall rule or a hung service. Nothing of it answering, and more than one # Service failing or the Server itself, is the Server or the way to it. What answered is # what a Finding cleared of Answered: it looked, and the Server was not unreachable. A # ping unanswered is the Server's own, beside its Services refused: the second Signal. # ADR-0015. [pscustomobject]@{ Name = 'server'; PerSubject = $true; Signals = @('server-lost', 'server-slow', 'server-unreachable', 'ping-unanswered', 'dependency-on-server') Answered = @('server-unreachable') } # Drives mapped by the domain's name, and one of the domain controllers behind it # that takes long: the drives come late on some days, and three Checks each say a # part of why. Seen on a desktop whose sign-in took ninety seconds every second day. A # folder of the namespace no Server serves any more is the drive's path dead (#172). [pscustomobject]@{ Name = 'domain-drives'; Signals = @('drive-mapping-slow', 'share-not-found', 'sign-in-intermittent', 'sign-in-by-controller', 'controller-differs', 'namespace-target-gone', 'namespace-unreachable', 'policies-differ') } # A clock that is off, and Servers that refused this machine's Kerberos tickets: the clock # is the cause. The clock is the machine's, so this is about no Server (#183). [pscustomobject]@{ Name = 'clock-refuses-kerberos'; Signals = @('clock-off', 'kerberos-refused') } # A Service that refused a sign-in, and beside it on the same Service what makes it refuse: # no credential saved, one it no longer takes, no SPN or one on two accounts, or the # Service in maintenance. Read Service by Service, keyed as the Report keys it (ADR-0013, # ADR-0015): a refusal on one Service and a cause on another are two stories (#183). # Without the refusal it is no Lead: a credential missing beside an SPN missing is two # causes of nothing that was seen to fail. An SPN is asked for an HTTPS Service only: # a Check that did not look for one is no gap beside a download over plain HTTP. [pscustomobject]@{ Name = 'service-sign-in'; PerService = $true; Requires = @('service-denied') Signals = @('service-denied', 'credential-refused', 'credential-missing', 'spn-missing', 'spn-duplicate', 'service-maintenance') HttpsOnly = @('spn-missing', 'spn-duplicate') } # The User's domain account locked out, or wrong passwords near a lockout, and beside it # what on this machine still holds the User's password: a drive, a saved credential, a # task or a service that keeps sending the old one after a change. The account is the # machine's User, so this is about no Server (#194). The lockout and the wrong passwords # are Effects: one of them must be seen, and the Lead joins it to a holder - the two are # one account read twice, and name no cause together. [pscustomobject]@{ Name = 'password-held'; Signals = @('account-locked', 'bad-passwords', 'password-held') Effects = @('account-locked', 'bad-passwords') } ) # How many Leads a Report shows. More than this at the top of a Report is a list again, # and a list is what a Lead is there to spare the reader. $script:LeadMaximum = 3 function Get-LeadText { <# .SYNOPSIS What a Lead is called and what to do about it. A rule this has no words for is called by its name, which shows that the words are missing. .DESCRIPTION Topic is what a note about a Lead that could not form names it by, where its Title says a conclusion: "the cause is found" stood in a note that no suspicion was possible. Without one, the Title is the Topic. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][string]$Name, [AllowNull()][AllowEmptyString()][string]$Subject, [AllowNull()]$Situation, # The Signals the Lead rests on. A rule says the same of whatever supports it, # but for one case: a connection that keeps failing over from cable to Wi-Fi and # back is said as that, because what to do about it is not what to do about any # connection that drops. [AllowNull()][AllowEmptyCollection()][string[]]$Signal, # Of a Lead about one Service: that the rest of its Server answered. [bool]$Answered, # Of the drives Lead: the roots of the Namespaces a folder of which no Server serves, # as the Findings it rests on name them in their parts (#172). [AllowNull()][AllowEmptyCollection()][string[]]$Namespace, # Of the Lead about a lockout: what holds the User's password, as the Findings it # rests on call it by its kind ("Laufwerk H:"), each as written (#194). [AllowNull()][AllowEmptyCollection()][string[]]$Holder, # The Run's Network Map, and the keys of the Server and the Service the Lead is about # on it: what names them as the Report does (Get-ReportServerName, # Get-ReportServiceName). Without the Map, a Server is named as its Finding wrote it. [AllowNull()]$NetworkMap, [AllowNull()][AllowEmptyString()][string]$ServerKey = '', [AllowNull()][AllowEmptyString()][string]$ServiceKey = '' ) switch ($Name) { 'service' { # The Service as a Technician knows it: SMB on fs01, not fs01:445; what its # Definition calls it, or the Apps that use it, where the Map knows them # (Get-ReportServiceName). A URL is what it is called, with that name before it # (Get-ReportServiceText). Its Server as the Report shows it: the Map's of the # Lead's key, else as written (Get-ReportServerName). $service = Split-ServiceSubject -Name $Subject if ($service -and $service.Url) { $server = Get-ReportServerName -Map $NetworkMap -Key $ServerKey -Name ([uri]$service.Url).Host $title = (Get-Text 'Lead.Service.Url.Title') -f (Get-ReportServiceText -Map $NetworkMap -Subject $Subject) } else { $server = Get-ReportServerName -Map $NetworkMap -Key $ServerKey -Name $(if ($service) { $service.Server } else { "$Subject" }) $known = Find-NetworkMapService -Map $NetworkMap -Key $ServiceKey $called = Get-ReportServiceName -Service $(if ($known) { $known } else { $service }) $port = $(if ($service) { $service.Port }) $what = $(if ($called) { (Get-Text 'Lead.Service.Port') -f $called, $port } else { (Get-Text 'Lead.Service.PortOnly') -f $port }) $title = (Get-Text 'Lead.Service.Title') -f $server, $what } $hint = (Get-Text 'Lead.Service.Hint') -f $server if ($Answered) { $hint = '{0} | {1}' -f ((Get-Text 'Lead.Service.Hint.Answers') -f $server), $hint } return [pscustomobject]@{ Title = $title; Hint = $hint } } 'connection' { if (@($Signal) -contains 'cable-wifi-switching') { return [pscustomobject]@{ Title = Get-Text 'Lead.Connection.Switching.Title'; Hint = Get-Text 'Lead.Connection.Switching.Hint' Topic = Get-Text 'Lead.Connection.Title' } } return [pscustomobject]@{ Title = Get-Text 'Lead.Connection.Title' Hint = (Get-Text 'Lead.Connection.Hint') -f (Get-SituationLinkText -Situation $Situation) } } 'server' { $server = Get-ReportServerName -Map $NetworkMap -Key $ServerKey -Name $Subject return [pscustomobject]@{ Title = (Get-Text 'Lead.Server.Title') -f $server; Hint = (Get-Text 'Lead.Server.Hint') -f $server } } 'domain-drives' { # Which controller it is, is known where the sign-ins part by it. $hint = Get-Text 'Lead.DomainDrives.Hint' if (@($Signal) -contains 'sign-in-by-controller') { $hint = '{0} | {1}' -f (Get-Text 'Lead.DomainDrives.Hint.ByController'), $hint } # A cause that was found is said in front of everything that is only to be checked. if (@($Signal) -contains 'policies-differ') { $hint = '{0} | {1}' -f (Get-Text 'Lead.DomainDrives.Hint.Policies'), $hint } if (@($Signal) -contains 'namespace-target-gone') { $hint = '{0} | {1}' -f (Get-Text 'Lead.DomainDrives.Hint.Namespace'), $hint } # And a path that is dead before a Server gone from a folder another one serves, # named by its Namespace where the Finding names one. if (@($Signal) -contains 'namespace-unreachable') { $roots = @($Namespace | Where-Object { $_ }) $dead = $(if ($roots.Count) { (Get-Text 'Lead.DomainDrives.Hint.Unreachable.Named') -f ($roots -join ', ') } else { Get-Text 'Lead.DomainDrives.Hint.Unreachable' }) $hint = '{0} | {1}' -f $dead, $hint } return [pscustomobject]@{ Title = Get-Text 'Lead.DomainDrives.Title'; Hint = $hint } } 'clock-refuses-kerberos' { return [pscustomobject]@{ Title = Get-Text 'Lead.KerberosClock.Title'; Hint = Get-Text 'Lead.KerberosClock.Hint'; Topic = Get-Text 'Lead.KerberosClock.Topic' } } 'service-sign-in' { # The Service as the Report names it (Get-ReportServiceText), and each cause that # was found said in front of what is to be done whatever it is: the one that # settles it first, maintenance before the directory before the credential. $called = Get-ReportServiceText -Map $NetworkMap -Subject $Subject if (-not $called) { $called = "$Subject" } $hint = Get-Text 'Lead.ServiceSignIn.Hint' foreach ($cause in @( @{ Signal = 'credential-missing'; Text = (Get-Text 'Lead.ServiceSignIn.Hint.CredentialMissing') } @{ Signal = 'credential-refused'; Text = (Get-Text 'Lead.ServiceSignIn.Hint.CredentialRefused') } @{ Signal = 'spn-missing'; Text = (Get-Text 'Lead.ServiceSignIn.Hint.SpnMissing') } @{ Signal = 'spn-duplicate'; Text = (Get-Text 'Lead.ServiceSignIn.Hint.SpnDuplicate') } @{ Signal = 'service-maintenance'; Text = (Get-Text 'Lead.ServiceSignIn.Hint.Maintenance') })) { if (@($Signal) -contains $cause.Signal) { $hint = '{0} | {1}' -f $cause.Text, $hint } } return [pscustomobject]@{ Title = (Get-Text 'Lead.ServiceSignIn.Title') -f $called; Hint = $hint; Topic = (Get-Text 'Lead.ServiceSignIn.Topic') -f $called } } 'password-held' { # Locked out, or only near it; and what to fix first, as the Findings call each # holder by its kind. A few of them, and how many more: the Findings name every one. $locked = @($Signal) -contains 'account-locked' -or -not (@($Signal) -contains 'bad-passwords') $title = $(if ($locked) { Get-Text 'Lead.PasswordHeld.Title' } else { Get-Text 'Lead.PasswordHeld.Title.Near' }) $hint = Get-Text 'Lead.PasswordHeld.Hint' $names = @($Holder | Where-Object { $_ }) if ($names.Count) { $shown = @($names | Select-Object -First 3) $rest = $names.Count - $shown.Count $list = $(if ($rest) { (Get-Text 'Lead.PasswordHeld.List') -f ($shown -join ', '), ((Get-Text 'Lead.PasswordHeld.More') -f $rest) } elseif ($shown.Count -gt 1) { (Get-Text 'Lead.PasswordHeld.List') -f ($shown[0..($shown.Count - 2)] -join ', '), $shown[-1] } else { $shown[0] }) $state = $(if ($locked) { Get-Text 'Lead.PasswordHeld.Locked' } else { Get-Text 'Lead.PasswordHeld.Near' }) $holders = $(if ($names.Count -eq 1) { Get-Text 'Lead.PasswordHeld.Holders.One' } else { Get-Text 'Lead.PasswordHeld.Holders' }) $hint = '{0} | {1}' -f ($holders -f $state, $list), $hint } return [pscustomobject]@{ Title = $title; Hint = $hint; Topic = Get-Text 'Lead.PasswordHeld.Topic' } } 'drive' { return [pscustomobject]@{ Title = Get-Text 'Lead.Drive.Title'; Hint = Get-Text 'Lead.Drive.Hint' } } 'memory' { return [pscustomobject]@{ Title = Get-Text 'Lead.Memory.Title'; Hint = Get-Text 'Lead.Memory.Hint' } } 'graphics' { return [pscustomobject]@{ Title = Get-Text 'Lead.Graphics.Title'; Hint = Get-Text 'Lead.Graphics.Hint' } } } [pscustomobject]@{ Title = $Name; Hint = '' } } function Get-Lead { <# .SYNOPSIS The Leads a Run's Findings support. Pure. .DESCRIPTION A Lead takes two Findings that observed two different things its rule names. One Finding has its own Meaning and Hint and needs no reading-together. The same thing observed twice - two drives that report wear, two Servers that are slow - is two Findings about two things, and no evidence that either is what the other means. A Finding that is in order observed nothing that supports anything - but for what a rule calls Background: how the machine is set, which is no fault and still counts beside one. Background alone starts nothing. Every rule that fits is stated, the one resting on more Findings first, and no more than a few: a Report that showed one of two would claim to know which it is. A rule about one Server reads together the Findings whose Subjects are one Server as the Report draws it: by the Key of the Network Map's Server, where the Run hands in its Map, else by the identity module's key (Get-NetworkMapServerKey, ADR-0013). A Subject that names a Service is that Service on that Server, keyed as the Map keys it (Get-NetworkMapServiceKey), and a Finding takes part for its Subject and for each of its parts that is not in order (ADR-0015). Where the Server answered apart from what fails on it, a Lead is about one Service, else about the Server. A rule about one Service (PerService) reads together what names that Service, by the same key, and nothing that names only its Server. A rule that Requires a Signal forms nothing where it was not seen: a cause without the refusal it causes. A rule with Effects - what was seen to fail, any one of several - forms nothing where none of them was seen, and a Lead only where a Signal beside them was: two Effects are the failure seen twice, and no cause. What was not looked for is then only what is not an Effect: an Effect unread would make no Lead beside the one seen. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Finding, [AllowNull()][AllowEmptyCollection()]$Rule = $script:LeadRules, # What a Lead's Hint may assume about the machine, like any Hint. [AllowNull()]$Situation, # The Run's Network Map (New-NetworkMap), whose Servers the Report draws: which # Subjects are one Server. Without it, the identity module says it alone. [AllowNull()]$NetworkMap ) $all = @($Finding | Where-Object { $_ }) $candidates = @($all | Where-Object { "$(Get-DataProperty $_ 'Severity')" -ne 'OK' }) # What every Finding is about, its parts too: what tells which FQDN a short name stands for. # Not a Namespace: its root is no Server, and its host no FQDN a short name is (#172). $subjects = @($all | ForEach-Object { "$(Get-DataProperty $_ 'Subject')"; (Get-DataCollection $_ 'Subjects') | ForEach-Object { "$(Get-DataProperty $_ 'Subject')" } } | Where-Object { $_ -and (Split-Subject -Name $_).Kind -ne 'Namespace' }) # What a Finding names: its Subject, and each of its parts - but a part in order of a # Finding that is not, which observed nothing wrong of what it names. $namesOf = { param($Of) $inOrder = "$(Get-DataProperty $Of 'Severity')" -eq 'OK' $parts = @((Get-DataCollection $Of 'Subjects') | Where-Object { $inOrder -or "$(Get-DataProperty $_ 'Severity')" -ne 'OK' } | ForEach-Object { "$(Get-DataProperty $_ 'Subject')" }) @(@("$(Get-DataProperty $Of 'Subject')") + $parts | Where-Object { $_ } | Select-Object -Unique) } # The Server each Subject is, and the Service on it where it names one, asked once for # each: fs01:445 is the Service fs01.contoso.example:445 on fs01.contoso.example, a URL # the Service of that URL on its host. A Namespace is on no Server: \\contoso.example\public # is judged by its folders, and is never the domain's Server (#172). $keys = @{} $keyOf = { param([string]$Subject) if (-not $keys.ContainsKey($Subject)) { $read = Split-Subject -Name $Subject if ($read.Kind -eq 'Namespace') { $keys[$Subject] = [pscustomobject]@{ Server = ''; Service = ''; ServerName = '' } } else { $server = $(if ($read.Url) { ([uri]$read.Url).Host } elseif ($read.Kind -eq 'Service') { $read.Server } else { $Subject }) $serverKey = Get-NetworkMapServerKey -Name $server -Among $subjects -Map $NetworkMap $serviceKey = '' if ($read.Kind -eq 'Service' -and $serverKey) { $serviceKey = Get-NetworkMapServiceKey -ServerKey $serverKey -Port $read.Port -Url $read.Url } $keys[$Subject] = [pscustomobject]@{ Server = $serverKey; Service = $serviceKey; ServerName = $server } } } $keys[$Subject] } $formed = New-Object 'System.Collections.Generic.List[object]' $open = New-Object 'System.Collections.Generic.List[object]' $order = 0 foreach ($one in @($Rule | Where-Object { $_ })) { $order++ $wanted = @((Get-DataCollection $one 'Signals') | ForEach-Object { "$_" }) $background = @((Get-DataCollection $one 'Background') | ForEach-Object { "$_" }) # What observed something: not in order, and about more than how the machine is set. $mine = @($candidates | Where-Object { @((Get-DataCollection $_ 'Signals') | Where-Object { $_ -in $wanted -and $_ -notin $background }).Count }) if (-not $mine.Count) { continue } if ($background.Count) { $mine += @($all | Where-Object { $mine -notcontains $_ -and @((Get-DataCollection $_ 'Signals') | Where-Object { $_ -in $background }).Count }) } # A rule about one thing among several is asked once for each of them, and a # Finding that is about nothing in particular takes part in none. One thing is one # Server as the Report draws it: the Network Map's, else a short name with the one # FQDN that begins with it among the Run's Subjects, two FQDNs never (ADR-0013). $ruleName = "$(Get-DataProperty $one 'Name')" $groups = @([pscustomobject]@{ Name = $ruleName; Subject = ''; Server = ''; Service = ''; Answered = $false; Findings = $mine }) if ([bool](Get-DataProperty $one 'PerSubject')) { # What fails on each Server: of its own - a ping, a program on it - and of each of # its Services. A Finding that names several is in each. $servers = [ordered]@{} foreach ($f in $mine) { foreach ($name in @(& $namesOf $f)) { $key = & $keyOf $name if (-not $key.Server) { continue } if (-not $servers.Contains($key.Server)) { $servers[$key.Server] = [pscustomobject]@{ Name = ''; Own = @(); Services = [ordered]@{}; Answered = $false } } $server = $servers[$key.Server] if (-not $key.Service) { if (-not $server.Name) { $server.Name = $name } if ($server.Own -notcontains $f) { $server.Own += $f } continue } if (-not $server.Services.Contains($key.Service)) { $server.Services[$key.Service] = [pscustomobject]@{ Name = $name; ServerName = $key.ServerName; Findings = @() } } if ($server.Services[$key.Service].Findings -notcontains $f) { $server.Services[$key.Service].Findings += $f } } } # What answered of each, apart from what fails on it: its ping, a Service of it # nothing failed on. Not a Signal, and no Lead forms from it: it says which Lead. $answering = @((Get-DataCollection $one 'Answered') | ForEach-Object { "$_" }) foreach ($f in @($all | Where-Object { $mine -notcontains $_ -and @((Get-DataCollection $_ 'Cleared') | Where-Object { $_ -in $answering }).Count })) { foreach ($name in @(& $namesOf $f)) { $key = & $keyOf $name if (-not $servers.Contains($key.Server)) { continue } if ($key.Service -and $servers[$key.Server].Services.Contains($key.Service)) { continue } $servers[$key.Server].Answered = $true } } # A Server that answered is up: each Service failing on it is a Lead of its own, # with what is about the Server itself beside it. Nothing answering, one Service # failing and nothing else is that Service; more is the Server or the way to it. $groups = @(foreach ($serverKey in @($servers.Keys | Sort-Object)) { $server = $servers[$serverKey] $services = @($server.Services.Keys | Sort-Object) $whole = -not $services.Count -or (-not $server.Answered -and ($server.Own.Count -or $services.Count -ge 2)) if ($whole) { $about = @($server.Own) + @($services | ForEach-Object { $server.Services[$_].Findings }) $named = $(if ($server.Name) { $server.Name } else { $server.Services[$services[0]].ServerName }) [pscustomobject]@{ Name = $ruleName; Subject = $named; Server = $serverKey; Service = ''; Answered = $server.Answered Findings = @($mine | Where-Object { $about -contains $_ }) } continue } foreach ($serviceKey in $services) { $about = @($server.Own) + @($server.Services[$serviceKey].Findings) [pscustomobject]@{ Name = 'service'; Subject = $server.Services[$serviceKey].Name; Server = $serverKey; Service = $serviceKey; Answered = $server.Answered Findings = @($mine | Where-Object { $about -contains $_ }) } } }) } elseif ([bool](Get-DataProperty $one 'PerService')) { # A rule about one Service is asked once for each, keyed as a Service Lead keys it: # its Server by the Map, its port or URL kept. What names a Server alone, or # nothing, is about no Service and takes part in none. $services = [ordered]@{} foreach ($f in $mine) { foreach ($name in @(& $namesOf $f)) { $key = & $keyOf $name if (-not $key.Service) { continue } if (-not $services.Contains($key.Service)) { $services[$key.Service] = [pscustomobject]@{ Name = $name; Server = $key.Server; Findings = @() } } if ($services[$key.Service].Findings -notcontains $f) { $services[$key.Service].Findings += $f } } } $groups = @(foreach ($serviceKey in @($services.Keys | Sort-Object)) { $service = $services[$serviceKey] [pscustomobject]@{ Name = $ruleName; Subject = $service.Name; Server = $service.Server; Service = $serviceKey; Answered = $false Findings = @($mine | Where-Object { $service.Findings -contains $_ }) } }) } $required = @((Get-DataCollection $one 'Requires') | ForEach-Object { "$_" }) $httpsOnly = @((Get-DataCollection $one 'HttpsOnly') | ForEach-Object { "$_" }) $effects = @((Get-DataCollection $one 'Effects') | ForEach-Object { "$_" }) foreach ($group in $groups) { $support = @($group.Findings) $seen = @{} foreach ($f in $support) { foreach ($signal in @((Get-DataCollection $f 'Signals') | Where-Object { $_ -in $wanted })) { $seen["$signal"] = $true } } # What the rule is about must have been seen, or there is nothing to join a cause to. if (@($required | Where-Object { -not $seen.ContainsKey($_) }).Count) { continue } if ($effects.Count -and -not @($effects | Where-Object { $seen.ContainsKey($_) }).Count) { continue } # What could have supported it and did not look: a Check that was skipped, # rights that were not given. Its Value says why. It supports nothing. Of a # rule about one thing, only what is about that thing or about none: of a # Service, what is about it, about its Server or about none. What matters for an # HTTPS Service only is no gap of a Service that is not one: port 443 or an https URL. $missed = @($wanted | Where-Object { $_ -notin $effects }) if ($group.Service -and $httpsOnly.Count) { $service = Split-ServiceSubject -Name $group.Subject $https = $service -and ($service.Port -eq 443 -or ($service.Url -and ([uri]$service.Url).Scheme -eq 'https')) if (-not $https) { $missed = @($missed | Where-Object { $_ -notin $httpsOnly }) } } $notChecked = @(foreach ($f in $all) { if (-not @((Get-DataCollection $f 'Unobserved') | Where-Object { $_ -in $missed }).Count) { continue } $about = @(& $namesOf $f | ForEach-Object { & $keyOf $_ } | Where-Object { $_.Server }) if ($group.Server -and $about.Count -and -not @($about | Where-Object { $_.Server -eq $group.Server -and (-not $group.Service -or -not $_.Service -or $_.Service -eq $group.Service) }).Count) { continue } # A Finding that speaks for several Checks - a Scope that left thirty out - # says which of them would have looked for this, and only those are named. $by = @((Get-DataCollection $f 'UnobservedBy') | ForEach-Object { $_ }) if ($by.Count) { foreach ($item in $by) { if (@((Get-DataCollection $item 'Signals') | Where-Object { $_ -in $missed }).Count) { [pscustomobject]@{ Check = "$(Get-DataProperty $item 'Check')"; Reason = "$(Get-DataProperty $item 'Reason')" } } } continue } [pscustomobject]@{ Check = "$(Get-DataProperty $f 'Check')"; Reason = "$(Get-DataProperty $f 'Value')" } }) $isLead = $support.Count -ge 2 -and $seen.Count -ge 2 -and (-not $effects.Count -or @($seen.Keys | Where-Object { $_ -notin $effects }).Count) # One observation and nobody looked for the second: not a Lead, and not nothing. if (-not $isLead -and -not $notChecked.Count) { continue } # A dead path by the part that names it, never by a Finding's words (ADR-0005): the # root of each Namespace a Finding that observed it names FAIL - its worst folder. $dead = @($support | Where-Object { (Get-DataCollection $_ 'Signals') -contains 'namespace-unreachable' } | ForEach-Object { (Get-DataCollection $_ 'Subjects') | Where-Object { "$(Get-DataProperty $_ 'Severity')" -eq 'FAIL' } } | ForEach-Object { Split-Subject -Name "$(Get-DataProperty $_ 'Subject')" } | Where-Object { $_.Kind -eq 'Namespace' } | ForEach-Object { $_.Key } | Select-Object -Unique) # What holds the User's password, as each Finding that found one calls it by its # kind (its Holder, which the Kind that judged it words), else by its Subject: a # Subject is a key and says no kind, and its shape is no way to tell one. $held = @($support | Where-Object { (Get-DataCollection $_ 'Signals') -contains 'password-held' } | ForEach-Object { $called = "$(Get-DataProperty $_ 'Holder')"; if ($called) { $called } else { "$(Get-DataProperty $_ 'Subject')" } } | Where-Object { $_ } | Select-Object -Unique) $text = Get-LeadText -Name $group.Name -Subject $group.Subject -Situation $Situation -Signal @($seen.Keys) -Answered $group.Answered -Namespace $dead ` -Holder $held -NetworkMap $NetworkMap -ServerKey $group.Server -ServiceKey $group.Service # When what it rests on was last seen: only where every Finding it rests on # says so - one that is about now would make any date a wrong one. A fault # that was repaired last week still fills the month the Findings read, and # without this a repaired machine reads as broken. $dates = @($support | ForEach-Object { ConvertTo-DataTime (Get-DataProperty $_ 'LastSeen') }) $lastSeen = $null if ($dates.Count -and -not @($dates | Where-Object { $null -eq $_ }).Count) { $lastSeen = @($dates | Sort-Object)[-1] $text.Hint = '{0} | {1}' -f ((Get-Text 'Lead.LastSeen') -f $lastSeen), $text.Hint } $lead = [pscustomobject]@{ PSTypeName = 'Gutcheck.Lead' Name = "$($group.Name)" Subject = "$($group.Subject)" Title = $text.Title # What a note that it could not form names it by (Get-LeadText). Topic = $(if ($text.PSObject.Properties['Topic'] -and $text.Topic) { $text.Topic } else { $text.Title }) Hint = $text.Hint Findings = $support LastSeen = $lastSeen Signals = @($seen.Keys | Sort-Object) NotChecked = $notChecked # $false: what was observed is one thing only, and what could have made a # Lead of it was not looked at. Said under the Leads, so that it is not # read as none. Formed = $isLead Order = $order } if ($isLead) { $formed.Add($lead) } else { $open.Add($lead) } } } # Sort-Object is not stable on Windows PowerShell 5.1, so the order rules were given # in is part of the key. @($formed | Sort-Object @{ Expression = { $_.Findings.Count }; Descending = $true }, Order | Select-Object -First $script:LeadMaximum) @($open | Sort-Object Order | Select-Object -First $script:LeadMaximum) } |