Private/Kinds/UserProfileElevated.ps1

# The UserProfileElevated Kind: the User Profiles of the machine, read with admin rights.
#
# The UserProfile Kind runs in the Main Part, in the Session of the User, and the profile
# folder of another account does not open for a User: how large it is stays unread. This
# Kind lists the same User Profiles with admin rights, measures the folder of each, and
# judges nothing: the UserProfile Check is handed its reading as
# $Observed['UserProfileElevated'] and fills in what it could not read itself.
#
# Of a User Profile the account, the kind, when it was last used, whether it is loaded,
# and one size: that of the profile folder as a whole (ADR-0006). No folder below it is
# told apart, no file is named or opened, and files are not counted. Where a User Profile
# lies and what Windows keeps of it in the registry is read, used for the measuring and
# the kind, and not kept: what this Kind gathers is written to disk and handed to the
# Main Part, and holds what a Report may hold of another User and no more. See
# ConvertTo-UserProfileOtherEntry.
#
# Its one Finding says that the reading was taken, so that the Check is accounted for in
# the Report like any other.

$script:UserProfileElevatedNeedsAdmin = $true

function Get-UserProfileElevatedData {
    <#
    .SYNOPSIS
        Every User Profile of a person on the machine and how large its folder is, as an
        administrator may read it. Decides nothing.
    .DESCRIPTION
        Listed as the UserProfile Kind lists them, by the same function, so that the two
        readings have one shape and one can fill in the other. Measured by the code that
        measures the folders of the own User Profile: see
        Initialize-UserProfileNativeMethod for what it follows and what it never opens.
 
        OtherMeasureSeconds (60) is how long the profile folders are measured for, all of
        them together. A rule of thumb and no limit of Windows: a minute is enough for a
        handful of User Profiles on a solid-state drive, and on a terminal server with a
        hundred of them no time is that anybody would wait for. What is not measured by
        then is said to be unmeasured, User Profile by User Profile: the one the time
        ran out in has a lower bound, those after it no number. Never more than ten
        minutes, whatever a Check Definition says. The Elevated Part is given this much
        longer: see Get-ElevatedExtraTimeout.
 
        Those that are not loaded are measured first. They are the ones a Technician may
        remove, and so the ones whose size is asked for.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $seconds = ConvertTo-Number (Get-Parameter $Parameters 'OtherMeasureSeconds' 60)
    if ($null -eq $seconds) { $seconds = 60 }
    $seconds = [int][math]::Min(600, [math]::Max(1, $seconds))

    # As Windows lists them, with where each lies: for the measuring, and no further.
    $read = @(Get-UserProfileOtherReading)

    $sizes = @{}
    try {
        $folders = @($read | Sort-Object @{ Expression = { [bool]$_.Loaded } }, Sid |
            ForEach-Object { [pscustomobject]@{ Id = $_.Sid; Path = $_.LocalPath } })
        Measure-UserProfileFolder -Folder $folders -ProfilePath '' -Seconds $seconds | ForEach-Object { $sizes["$($_.Id)"] = $_ }
    }
    catch { $sizes = @{} }

    # What is kept, and what crosses to the Main Part: see ConvertTo-UserProfileOtherEntry.
    $profiles = @(foreach ($entry in $read) {
        ConvertTo-UserProfileOtherEntry -Row $entry -Size $sizes[$entry.Sid] -Measured
    })

    [pscustomobject]@{
        PSTypeName          = 'Gutcheck.Data.UserProfileElevated'
        Profiles            = $profiles
        OtherMeasureSeconds = $seconds
        GatheredAt          = Get-Date
    }
}

function ConvertTo-UserProfileElevatedFinding {
    <#
    .SYNOPSIS
        One Finding saying how many User Profiles were read with admin rights. Pure.
    .DESCRIPTION
        No judgement: which of them count as unused, and whether that matters, is the
        UserProfile Check's to judge, with the limits of its Definition.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data, [hashtable]$Parameters = @{})

    $profiles = @((Get-DataCollection $Data 'Profiles') | ForEach-Object { $_ })
    if (-not $profiles.Count) {
        return New-UnavailableFinding -Category User -Check (Get-Text 'Check.UserProfileElevated.Reading') `
            -Hint (Get-Text 'Hint.UserProfile.Repeat')
    }

    $measured = @($profiles | Where-Object { "$(Get-DataProperty $_ 'SizeState')" -in 'Complete', 'CutShort' }).Count
    New-Finding -Category User -Check (Get-Text 'Check.UserProfileElevated.Reading') -Severity OK `
        -Value ((Get-Text 'Value.UserProfileElevated.Reading') -f $profiles.Count, $measured)
}