Private/Scope.ps1
|
# A Scope: a named part of the Checks, for a Run that is to look at one thing. # # A Run performs every Check of the Check Definitions, and that is what a Run is when # nobody says otherwise. A Technician with a complaint about the network does not need # the stress test, and one who is asked whether the disk is failing does not need the # sign-in of the last month. A Scope names the Checks for such a question, and a Run # that is given one performs those and no others. # # Scopes are data, in the Check Definition document beside the Checks (ADR-0001): # # "Scopes": [ { "Name": "Hardware", what -Scope takes # "Title": "Hardware", what the Technician reads # "Checks": [ "System", "Memory" ], the Checks, by their Name # "Apps": false }, whether the applications take part # { "Name": "Quick", "Title": "Schnell", "Except": [ "CPU stress test" ] } ] # # "Checks" names what is in; "Except" names what is out of everything. Apps is true where # it is not said. Several Scopes at once are all their Checks together. # # A Check that is left out leaves no Finding of its own - thirty lines of "not performed" # would bury the Report. One Finding says what the Scope was and what it left out, and # carries as Unobserved every Signal the omitted Checks would have looked for, so that a # Lead says "not checked" and never "nothing found" (ADR-0005). # # Some Checks read what another gathered, and judge differently without it. A Scope that # holds the reader and not its source is a mistake in the Definitions; it is found by a # test over the Local Definitions, and said in the Finding for any others. # Which Kinds a Kind reads, where going without changes what it says and it does not say # so, or where one of a pair is no use without the other. Not every reading: a Kind that # reads another only to save a second look is not here. $script:ScopeReadsFrom = @{ Program = @('ProgramElevated') ProgramElevated = @('Program') Session = @('SessionElevated') SessionElevated = @('Session') UserProfile = @('UserProfileElevated', 'Storage', 'NetworkDependency') UserProfileElevated = @('UserProfile') NetworkDependency = @('NetworkDependencyElevated') NetworkDependencyElevated = @('NetworkDependency') Network = @('NetworkDisruption') Logon = @('NetworkDisruption') Stability = @('NetworkDisruption', 'DiskHealth') AppRuntime = @('DotNet') App = @('Stability') Service = @('Stability') Companion = @('Stability') } function ConvertTo-ScopeDefinition { <# .SYNOPSIS One entry of a document's Scopes as a Run uses it. Nothing for an entry without a name. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Entry) $name = "$(Get-DataProperty $Entry 'Name')".Trim() if (-not $name) { return } $title = "$(Get-DataProperty $Entry 'Title')".Trim() if (-not $title) { $title = $name } $apps = Get-DataProperty $Entry 'Apps' [pscustomobject]@{ PSTypeName = 'Gutcheck.ScopeDefinition' Name = $name Title = $title Checks = [string[]]@(@(Get-DataProperty $Entry 'Checks') | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) Except = [string[]]@(@(Get-DataProperty $Entry 'Except') | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) Apps = $(if ($null -eq $apps) { $true } else { [bool]$apps }) } } function Select-ScopeCheck { <# .SYNOPSIS Narrows the Check Definitions of a Run to the Scopes it was given. Pure. .DESCRIPTION Without a Scope everything comes back as it went in. With one or several: the Checks about the machine that any of them names, in the order of the document. The Checks of the applications stay where any of the Scopes takes applications, or the Technician asked for applications by name; which of them run is then the application chooser's to say, as in any Run. Returns what to perform, what was left out, and what is wrong with the Scopes: a name no Scope has, a Check no Definition has, and a Check that reads another which is not among them. .PARAMETER KeepApps The Technician named applications: they take part whatever the Scope says. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Definition, [AllowNull()][AllowEmptyCollection()]$Scope, [AllowNull()][AllowEmptyCollection()][string[]]$Requested = @(), [switch]$KeepApps ) $all = @($Definition | Where-Object { $_ }) $known = @($Scope | Where-Object { $_ }) $asked = @($Requested | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) $chosen = @($known | Where-Object { $asked -contains $_.Name }) $unknown = @($asked | Where-Object { $name = $_; -not @($known | Where-Object { $_.Name -eq $name }).Count }) if (-not $chosen.Count) { return [pscustomobject]@{ PSTypeName = 'Gutcheck.ScopeSelection' Scope = @(); Check = $all; Omitted = @(); OmittedApps = @(); Apps = $true UnknownScope = $unknown; UnknownCheck = @(); Unmet = @() } } $isApp = { param($One) [bool](Get-DefinitionApp -Definition $One) -or "$(Get-DataProperty $One 'Kind')" -eq 'App' } $machine = @($all | Where-Object { -not (& $isApp $_) }) $names = @($machine | ForEach-Object { "$(Get-DataProperty $_ 'Name')" }) $wanted = [System.Collections.Generic.List[string]]::new() foreach ($one in $chosen) { $take = @($one.Checks) # "Except", or neither: everything about the machine but what is named. if (-not $take.Count) { $take = @($names | Where-Object { $one.Except -notcontains $_ }) } foreach ($name in $take) { if (-not $wanted.Contains($name)) { $wanted.Add($name) } } } $unknownCheck = @($chosen | ForEach-Object { @($_.Checks) + @($_.Except) } | Where-Object { $names -notcontains $_ } | Select-Object -Unique) $apps = [bool]$KeepApps -or [bool]@($chosen | Where-Object { $_.Apps }).Count $kept = @($all | Where-Object { if (& $isApp $_) { $apps } else { $wanted -contains "$(Get-DataProperty $_ 'Name')" } }) $omitted = @($machine | Where-Object { $wanted -notcontains "$(Get-DataProperty $_ 'Name')" }) # A reader among the Checks whose source is not. $kinds = @($kept | ForEach-Object { "$(Get-DataProperty $_ 'Kind')" } | Select-Object -Unique) $unmet = @(foreach ($kind in $kinds) { foreach ($source in @($script:ScopeReadsFrom[$kind])) { if ($source -and $kinds -notcontains $source) { # Only where the document has such a Check at all: what it does not carry # no Scope can be blamed for leaving out. $has = @($all | Where-Object { "$(Get-DataProperty $_ 'Kind')" -eq $source })[0] $reader = @($kept | Where-Object { "$(Get-DataProperty $_ 'Kind')" -eq $kind })[0] if ($has) { [pscustomobject]@{ Reader = "$(Get-DataProperty $reader 'Name')"; Source = "$(Get-DataProperty $has 'Name')" } } } } }) [pscustomobject]@{ PSTypeName = 'Gutcheck.ScopeSelection' Scope = $chosen Check = $kept Omitted = $omitted # The Checks of the applications, where the Scope takes none: left out too, and # they look for things of their own - whether a Server answers, above all. OmittedApps = @($all | Where-Object { (& $isApp $_) -and -not $apps }) Apps = $apps UnknownScope = $unknown UnknownCheck = $unknownCheck Unmet = $unmet } } function Read-ScopeSelection { <# .SYNOPSIS Asks the Technician what the Run is to look at: everything, or one of the Scopes. Returns the names chosen - none for everything. .DESCRIPTION Everything is where the pointer starts, so Enter is the Run it always was, and that is what comes back where nobody can be asked. The asking is Lantern's. #> [CmdletBinding()] [OutputType([string[]])] param([AllowNull()][AllowEmptyCollection()]$Scope) $known = @($Scope | Where-Object { $_ }) if (-not $known.Count) { return @() } $items = @([pscustomobject]@{ Name = ''; Title = (Get-Text 'Console.Scope.Everything'); Detail = '' }) + @($known | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Title = $_.Title; Detail = "$($_.Name)" } }) $chosen = Read-LanternChoice (Get-Text 'Console.Scope.Heading') -Item $items -Label { $_.Title } -Detail { $_.Detail } -Default { -not $_.Name } if ($null -eq $chosen -or -not $chosen.Name) { return @() } @($chosen.Name) } function Resolve-ScopeCheck { <# .SYNOPSIS The Checks of this Run by the Scope it was given, or by the one the Technician chooses where none was given and somebody is there to ask. .DESCRIPTION A name no Scope has stops the Run: a mistyped Scope must not become a Run of everything that somebody then takes for the narrow one. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Definition, [AllowNull()][AllowEmptyCollection()]$Scope, [AllowNull()][AllowEmptyCollection()][string[]]$Requested = @(), [switch]$KeepApps ) $asked = @($Requested | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) if (-not $asked.Count -and @($Scope).Count -and (Test-RunInteractive)) { $asked = @(Read-ScopeSelection -Scope $Scope) } $selection = Select-ScopeCheck -Definition $Definition -Scope $Scope -Requested $asked -KeepApps:$KeepApps if (@($selection.UnknownScope).Count) { $names = @($Scope | Where-Object { $_ } | ForEach-Object { $_.Name }) -join ', ' if (-not $names) { $names = Get-Text 'Value.Shared.None' } throw ((Get-Text 'Error.Scope.Unknown') -f (@($selection.UnknownScope) -join ', '), $names) } $selection } function Get-ScopeSuggestion { <# .SYNOPSIS The Scope to run again in, so that a set of Checks looks together: the one with the fewest Checks that holds every one named, or a Run without a Scope where none does. Nothing where no Check is named. Pure. .DESCRIPTION Asked for a suspicion that could not form because the Scope left out what would have formed it. What was seen has to be looked at again in the same Run as what was not, or the two never meet: both are named. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Definition, [AllowNull()][AllowEmptyCollection()]$Scope, [AllowNull()][AllowEmptyCollection()][string[]]$Check = @() ) $needed = @($Check | ForEach-Object { "$_".Trim() } | Where-Object { $_ } | Select-Object -Unique) if (-not $needed.Count) { return } $all = @($Definition | Where-Object { $_ }) $best = $null foreach ($one in @($Scope | Where-Object { $_ })) { $holds = @((Select-ScopeCheck -Definition $all -Scope $Scope -Requested @($one.Name)).Check | ForEach-Object { "$(Get-DataProperty $_ 'Name')" }) if (@($needed | Where-Object { $holds -notcontains $_ }).Count) { continue } if ($null -eq $best -or $holds.Count -lt $best.Count) { $best = [pscustomobject]@{ Name = $one.Name; Title = $one.Title; Count = $holds.Count } } } if ($best) { return $best } [pscustomobject]@{ Name = ''; Title = ''; Count = $all.Count } } function Get-ScopeTitle { <# .SYNOPSIS What a Run's Scope is called in the Report and in the window: the titles of the Scopes it was given, or nothing for a Run of everything. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Selection) @((Get-DataCollection $Selection 'Scope') | ForEach-Object { "$(Get-DataProperty $_ 'Title')" } | Where-Object { $_ }) -join ', ' } function New-ScopeFinding { <# .SYNOPSIS The Finding that says a Run was narrowed: to what, and what it therefore did not look at. Nothing for a Run of everything. Pure. .DESCRIPTION Its Unobserved are the Signals of every Kind that was left out, so that whatever reads Findings together knows of each that nobody looked. #> [CmdletBinding()] param([AllowNull()]$Selection) $title = Get-ScopeTitle -Selection $Selection if (-not $title) { return } $omitted = @((Get-DataCollection $Selection 'Omitted') | ForEach-Object { $_ }) $value = @((Get-Text 'Value.Scope.Chosen') -f $title, @((Get-DataCollection $Selection 'Check') | ForEach-Object { $_ }).Count, $omitted.Count) if ($omitted.Count) { $value += (Get-Text 'Value.Scope.Omitted') -f (@($omitted | ForEach-Object { "$(Get-DataProperty $_ 'Name')" }) -join '; ') } if (-not [bool](Get-DataProperty $Selection 'Apps')) { $value += Get-Text 'Value.Scope.NoApps' } $hint = @(Get-Text 'Hint.Scope.Chosen') $unmet = @((Get-DataCollection $Selection 'Unmet') | ForEach-Object { $_ }) if ($unmet.Count) { $value += (Get-Text 'Value.Scope.Unmet') -f (@($unmet | ForEach-Object { (Get-Text 'Value.Scope.UnmetItem') -f "$($_.Reader)", "$($_.Source)" }) -join '; ') $hint += Get-Text 'Hint.Scope.Unmet' } $strange = @((Get-DataCollection $Selection 'UnknownCheck') | ForEach-Object { "$_" } | Where-Object { $_ }) if ($strange.Count) { $value += (Get-Text 'Value.Scope.UnknownCheck') -f ($strange -join '; ') $hint += Get-Text 'Hint.Scope.UnknownCheck' } # Which Check would have looked for what: so that a Lead names the one or two that # matter to it, and not the whole list of what a Scope left out. $reason = (Get-Text 'Value.Scope.NotInScope') -f $title $by = @($omitted | ForEach-Object { $mine = @(Get-SignalOfKind -Kind "$(Get-DataProperty $_ 'Kind')" | Where-Object { $_ }) if ($mine.Count) { [pscustomobject]@{ Check = "$(Get-DataProperty $_ 'Name')"; Signals = $mine; Reason = $reason } } }) $appSignals = @((Get-DataCollection $Selection 'OmittedApps') | ForEach-Object { Get-SignalOfKind -Kind "$(Get-DataProperty $_ 'Kind')" } | Where-Object { $_ } | Select-Object -Unique) if ($appSignals.Count) { $by += [pscustomobject]@{ Check = (Get-Text 'Value.Scope.Apps'); Signals = $appSignals; Reason = $reason } } $signals = @($by | ForEach-Object { $_.Signals } | Select-Object -Unique) $finding = New-Finding -Category Gutcheck -Check (Get-Text 'Check.Scope.Scope') -Severity INFO -Value ($value -join ' | ') ` -Meaning (Get-Text 'Meaning.Scope.Chosen') -Hint ($hint -join ' | ') -Unobserved $signals $finding | Add-Member -NotePropertyName UnobservedBy -NotePropertyValue $by -PassThru } |