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. $script:CategoryValues = @( 'System', 'Power', 'CPU', 'Memory', 'Storage', 'Network', 'Stability', 'Security', 'Updates', 'Startup', 'Integrity', 'Apps', 'Access', '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 = '', # 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' ) $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 if ($severityValue -eq 'OK') { $hintValue = '' } [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 = '' } } 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' } |