Private/Finding.ps1
|
# The Finding primitive. # # A Finding is one judged observation about the Target Machine. Creating one returns a # value: it records nothing and prints nothing. Collecting Findings, ordering them and # showing them is the caller's job. # The four permitted Severity values, worst first. The Report orders by this array. $script:SeverityValues = @('FAIL', 'WARN', 'INFO', 'OK') # The areas a Finding can belong to. Closed, like Severity and Privilege, because the # Report groups by Category: a misspelling in one Kind would otherwise add a group rather # than fail, and a Technician would read two Storage sections and trust both. Carried over # from the Gutcheck script, except that its 'Script' is 'Gutcheck' here - Findings about # the Run itself outlive the script #16 deletes. A new Category needs a module release, # which is the rule a new Kind already follows. # # 'User' is the one added since: the Session, the User Profile and the settings of the # User of the Run. The Report calls it "Benutzer"; see Get-ReportCategoryTitle. $script:CategoryValues = @( 'System', 'Power', 'CPU', 'Memory', 'Storage', 'Network', 'Stability', 'Security', 'Updates', 'Startup', 'Integrity', 'Apps', 'Access', 'User', 'Gutcheck' ) function New-Finding { [CmdletBinding()] [OutputType([psobject])] param( # The Check this Finding came from, named as the Technician recognises it. [Parameter(Mandatory)][string]$Check, # The area of the machine the Finding belongs to; the Report groups by it. [Parameter(Mandatory)][string]$Category, # What the Check observed. Rendered to a string here so no later stage has to. [AllowNull()][AllowEmptyString()]$Value, [ValidateSet('OK', 'INFO', 'WARN', 'FAIL')][string]$Severity = 'INFO', # What a first-level Technician should do about it. Absent on OK Findings. [AllowEmptyString()][string]$Hint = '', # What the Value means, where it does not say so itself. What happened, not what # to do: that is the Hint. Absent on OK Findings, like the Hint. [AllowNull()][AllowEmptyString()][string]$Meaning = '', # The rights the data was gathered with. A Judge cannot know this, so it defaults # to the weaker of the two and the Part stamps the truth on with Set-FindingPrivilege. [ValidateSet('user', 'admin')][string]$Privilege = 'user', # What a Technician should read with this Finding. See New-Reference. [AllowNull()][AllowEmptyCollection()]$Reference, # What this Finding observed, by names from the list of Signals. Declared when the # thing was observed, not when it was looked for and not found. [AllowNull()][AllowEmptyCollection()][string[]]$Signal, # What the observation is about, where one Check makes a Finding for each of # several: the Server, the program, the drive. [AllowNull()][AllowEmptyString()][string]$Subject = '', # What this Finding would have observed and could not look for: rights it did not # have, a Check that was skipped. Its Value says why. [AllowNull()][AllowEmptyCollection()][string[]]$Unobserved ) Assert-Signal -Signal $Signal Assert-Signal -Signal $Unobserved $severityValue = $Severity.ToUpperInvariant() # Validated here rather than with ValidateSet so the permitted values live in one # place, and so the error can say what was wrong and what was allowed. $categoryValue = $script:CategoryValues | Where-Object { $_ -eq $Category } | Select-Object -First 1 if (-not $categoryValue) { throw ("'{0}' is not a Gutcheck Category. Permitted: {1}." -f $Category, ($script:CategoryValues -join ', ')) } # A Hint tells a Technician what to do about a Finding, so an OK Finding has none. # Dropping it here lets a Judge decide Severity and Hint together and pass both # unconditionally, which is how the thresholds read most plainly. $hintValue = $Hint $meaningValue = "$Meaning" if ($severityValue -eq 'OK') { $hintValue = ''; $meaningValue = '' } [pscustomobject]@{ PSTypeName = 'Gutcheck.Finding' Severity = $severityValue Category = $categoryValue Check = $Check Value = "$Value" Hint = $hintValue Privilege = $Privilege.ToLowerInvariant() # Which Check produced it, and for which application. A Judge cannot know either: # the Name is in the Check Definition and one Kind is named by many. The Run # stamps both on with Set-CheckOrigin, as it stamps Privilege. CheckName = '' App = '' # Kept on an OK Finding too: the evidence is as much its evidence. References = @($Reference | Where-Object { $null -ne $_ }) Signals = @($Signal | Where-Object { $_ } | Select-Object -Unique) Subject = "$Subject" Meaning = $meaningValue Unobserved = @($Unobserved | Where-Object { $_ } | Select-Object -Unique) } } function New-Reference { <# .SYNOPSIS A pointer from a Finding to something a Technician should read with it. .DESCRIPTION Declared on the Finding and not written into its Hint: "see the list below" sent a Technician through fifty folds looking for a list that was called something else, and a title quoted in a sentence breaks without a sound when the title changes. A Section is named by the key of its title. The Reference carries the title as it reads, because that is what the Report finds the Section by; a key that does not exist throws here, where a Judge is written, and not on a Customer's machine. .PARAMETER Argument What a title that takes a name or a number is filled in with. .PARAMETER Signal Refers to the other Findings that carry this Signal. A Finding has no name of its own to be referred to by: its Check is wording, and one Check makes many Findings. .PARAMETER SameSubject Only those about the same thing as the Finding that refers: the same Server, the same program. #> [CmdletBinding(DefaultParameterSetName = 'Section')] [OutputType([psobject])] param( [Parameter(Mandatory, ParameterSetName = 'Section')][string]$Section, [Parameter(ParameterSetName = 'Section')][AllowNull()][object[]]$Argument, [Parameter(Mandatory, ParameterSetName = 'Signal')][string]$Signal, [Parameter(ParameterSetName = 'Signal')][switch]$SameSubject ) if ($PSCmdlet.ParameterSetName -eq 'Signal') { Assert-Signal -Signal $Signal return [pscustomobject]@{ Target = 'Signal'; Signal = $Signal; SameSubject = [bool]$SameSubject } } $title = Get-Text $Section if ($Argument) { $title = $title -f $Argument } [pscustomobject]@{ Target = 'Section'; Title = $title } } function Set-CheckOrigin { <# .SYNOPSIS Stamps onto a Finding or a Section which Check produced it, and for which application. .DESCRIPTION "Abstuerze / .NET-Fehler / Haenger: 0 / 2 / 0" does not say whose crashes, and five Sections titled "Laufzeitmessungen (Server)" do not say whose Servers. The Check Definition knows: its Name, and for an application's Check the application its parameters name. A Report can only say what it is talking about if every Finding and Section carries that. Works on whatever it is handed, because what comes back from the Elevated Part is a property bag and not the object this module built. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(ValueFromPipeline)]$InputObject, [AllowNull()][AllowEmptyString()][string]$CheckName, [AllowNull()][AllowEmptyString()][string]$App, # For a Section: the Kind of the Check, which the Report groups the evidence by. [AllowNull()][AllowEmptyString()][string]$Kind ) process { if ($null -eq $InputObject) { return } $InputObject | Add-Member -NotePropertyName 'CheckName' -NotePropertyValue "$CheckName" -Force $InputObject | Add-Member -NotePropertyName 'App' -NotePropertyValue "$App" -Force if ($PSBoundParameters.ContainsKey('Kind')) { $InputObject | Add-Member -NotePropertyName 'Kind' -NotePropertyValue "$Kind" -Force } $InputObject } } function New-UnavailableFinding { <# .SYNOPSIS The Finding a Judge returns when the data it needed is not there. .DESCRIPTION A counter that would not load, a device that would not answer, an empty collection: a real machine produces all three, and none of them is a clean result. Every Judge says so the same way, so a Technician learns one phrase rather than one per Check. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][string]$Check, [Parameter(Mandatory)][string]$Category, [AllowEmptyString()][string]$Hint = '' ) New-Finding -Category $Category -Check $Check -Value (Get-Text 'Value.Shared.NotAvailable') -Severity INFO -Hint $Hint } function Set-FindingPrivilege { <# .SYNOPSIS Stamps the Privilege a Part gathered with onto the Findings it produced. .DESCRIPTION Judges are pure and cannot know what rights their data was gathered with, so a Part stamps its own Privilege onto everything it collected. This is the single place Privilege is assigned, on both the Main and the Elevated Part. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory, ValueFromPipeline)]$Finding, [Parameter(Mandatory)][ValidateSet('user', 'admin')][string]$Privilege ) process { $Finding.Privilege = $Privilege.ToLowerInvariant() $Finding } } function Get-CurrentPrivilege { <# .SYNOPSIS The Privilege this process gathers data with. .DESCRIPTION Not the same question as which Part is running: a Main Part that the Technician started elevated carries admin Privilege. #> [CmdletBinding()] [OutputType([string])] param() $identity = [Security.Principal.WindowsIdentity]::GetCurrent() if (([Security.Principal.WindowsPrincipal]$identity).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator)) { return 'admin' } 'user' } |