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()
    }
}

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