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'
}