Private/Kinds/Program.ps1
|
# The Program Kind: every program running on the machine, and what it holds. # # The App Kind answers for the applications somebody wrote a Check Definition for. A # line-of-business program nobody did can sit at the limit of GDI objects, or leak handles # all day, and no Check would say a word. This one looks at all of them: a table of every # running program, and a Finding only for one over a limit. # # A program an application's own Check judges in this Run is in the table and gets no # Finding here. Which those are the Run says, as $Observed['AppSelection']: this Check # belongs to the machine and runs before the applications' Checks, so it cannot ask what # they found. # # What this session may not open - the processes of another account - the ProgramElevated # Check read with admin rights, where the Elevated Part ran. Its reading fills in. # # Get-ProgramData reads the machine and decides nothing. ConvertTo-ProgramFinding and # ConvertTo-ProgramSection are pure. # What Windows runs as a process and is none: the kernel and what it keeps for itself. # They hold handles by the ten thousand by their nature and are not judged. They have no # file: a program that only bears one of these names has one, and is judged. $script:ProgramNotJudgedPattern = '^(System|Idle|Registry|Memory Compression|Secure System)$' function Get-ProgramData { <# .SYNOPSIS Every running process and what it holds, and the applications that have a Check of their own in this Run. Decides nothing. .PARAMETER Observed What the Run knows before this Check. 'AppSelection' is the applications the Technician chose, each with the pattern of its processes. 'ProgramElevated' is the same processes as an administrator may read them, absent where that Check was not performed. #> [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}, [AllowNull()][hashtable]$Observed = @{}) $own = @() if ($Observed -and $Observed.ContainsKey('AppSelection')) { $own = @((Get-DataCollection $Observed['AppSelection'] 'Selected') | ForEach-Object { $_ }) } # Copied across as it came. Which of it fills in what is for the pure half to say. $elevated = $null if ($Observed -and $Observed.ContainsKey('ProgramElevated')) { $elevated = $Observed['ProgramElevated'] } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Program' # Read as the App Kind reads an application's, of every process there is. Processes = @(Get-AppProcess -Pattern '.') OwnChecks = $own ElevatedRead = ($null -ne $elevated) ElevatedProcesses = @((Get-DataCollection $elevated 'Processes') | ForEach-Object { $_ }) } } function Join-ProgramReading { <# .SYNOPSIS The processes as this session read them, with what it could not open filled in from the reading taken with admin rights. Pure. .DESCRIPTION Only what was unread is filled in, and only from the same process: the same number and the same name, as a number is given to another process once its first owner has ended. Memory and handles stay this session's, read later. The filling in itself is Join-ElevatedReading's, which the Session Kind fills in by as well. Here is said what it is for and from what: for a process this session could not open, from the same process as the Elevated Part opened it. One that did not open with admin rights either has nothing to give. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) Join-ElevatedReading -Row (Get-DataCollection $Data 'Processes') -Elevated (Get-DataCollection $Data 'ElevatedProcesses') ` -Key 'ProcessId' -Take 'Gdi', 'User', 'Bits', 'Path' ` -Unread { param($Process) $null -eq (Get-DataProperty $Process 'Gdi') -or $null -eq (Get-DataProperty $Process 'User') } ` -Same { param($Process, $Other) $null -ne (Get-DataProperty $Other 'Gdi') -and "$(Get-DataProperty $Other 'Process')" -eq "$(Get-DataProperty $Process 'Process')" } | ForEach-Object { $_.Row } } function ConvertTo-ProgramCount { <# .SYNOPSIS A count as a whole number, and nothing as nothing: a maximum comes back as a floating-point number, and a table would show "830,00" objects. Pure. #> [CmdletBinding()] param([AllowNull()]$Value) if ($null -eq $Value) { return $null } [long]$Value } function ConvertTo-ProgramRow { <# .SYNOPSIS The processes as programs: one row per program, its processes together. Pure. .DESCRIPTION A program is a name and the file it runs from: "update" and "setup" are many programs' names. Processes whose file could not be read stand together under their name. Memory is the sum. Handles, GDI and USER objects are those of the largest process, because each limit is one per process. A program one of whose processes could not be opened is Unread: what is shown of it is what could be read. OwnCheck names the application's Check that judges it, NotJudged that it is the kernel's. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $own = @((Get-DataCollection $Data 'OwnChecks') | ForEach-Object { $_ }) $rows = @(Join-ProgramReading -Data $Data | Where-Object { "$(Get-DataProperty $_ 'Process')".Trim() } | Group-Object { ('{0}|{1}' -f "$(Get-DataProperty $_ 'Process')".Trim(), "$(Get-DataProperty $_ 'Path')".Trim()).ToLowerInvariant() } | ForEach-Object { $group = @($_.Group) $name = "$(Get-DataProperty $group[0] 'Process')".Trim() $path = "$(Get-DataProperty $group[0] 'Path')".Trim() $ownCheck = '' $ownApp = '' foreach ($check in $own) { $pattern = "$(Get-DataProperty $check 'ProcessPattern')" if (-not $pattern) { continue } $matched = $false try { $matched = $name -match $pattern } catch { } if ($matched) { $ownCheck = "$(Get-DataProperty $check 'CheckName')" $ownApp = "$(Get-DataProperty $check 'App')" if (-not $ownApp) { $ownApp = $ownCheck } break } } $memory = (@($group | ForEach-Object { ConvertTo-Number (Get-DataProperty $_ 'PrivateMB') } | Where-Object { $null -ne $_ }) | Measure-Object -Sum).Sum if ($null -eq $memory) { $memory = 0 } [pscustomobject]@{ Program = $name Processes = $group.Count PrivateMB = [long]$memory Handles = ConvertTo-ProgramCount (Get-AppMaximum -Row $group -Property 'Handles') Gdi = ConvertTo-ProgramCount (Get-AppMaximum -Row $group -Property 'Gdi') User = ConvertTo-ProgramCount (Get-AppMaximum -Row $group -Property 'User') Bits = (@($group | ForEach-Object { Get-DataProperty $_ 'Bits' } | Where-Object { $_ } | Select-Object -Unique | Sort-Object) -join '/') Path = $path Unread = [bool]@($group | Where-Object { $null -eq (Get-DataProperty $_ 'Gdi') -or $null -eq (Get-DataProperty $_ 'User') }).Count OwnCheck = $ownCheck OwnApp = $ownApp NotJudged = [bool](-not $path -and $name -match $script:ProgramNotJudgedPattern) } }) # Two keys, so that the order does not depend on how a sort treats equals. @($rows | Sort-Object @{ Expression = 'PrivateMB'; Descending = $true }, @{ Expression = 'Program'; Descending = $false }, @{ Expression = 'Path'; Descending = $false }) } function ConvertTo-ProgramFinding { <# .SYNOPSIS A Finding for each program over a limit, and one saying how many were looked at. Pure. .DESCRIPTION GuiObjectFailAbove (8,000) is the App Kind's. Windows gives a process 10,000 GDI and 10,000 USER objects (the registry values GDIProcessHandleQuota and USERProcessHandleQuota, as Microsoft documents under "GDI Objects" and "User Objects"), and at the limit it cannot draw any more. HandleWarnAbove (20,000) is not the App Kind's 10,000, and no limit of Windows: a process may hold about sixteen million handles (Microsoft, "Pushing the Limits of Windows: Handles"). It is a rule of thumb for "unusual", set above what a mail program holds on an ordinary day - a little under 15,000 on the machine this was written on - because this Check judges every program and would warn on most machines otherwise. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [hashtable]$Parameters = @{}) $guiFail = ConvertTo-Number (Get-Parameter $Parameters 'GuiObjectFailAbove' 8000) $handleWarn = ConvertTo-Number (Get-Parameter $Parameters 'HandleWarnAbove' 20000) if ($null -eq $guiFail) { $guiFail = 8000 } if ($null -eq $handleWarn) { $handleWarn = 20000 } $rows = @(ConvertTo-ProgramRow -Data $Data) if (-not $rows.Count) { return New-UnavailableFinding -Category Apps -Check (Get-Text 'Check.Program.Summary') ` -Hint (Get-Text 'Hint.Program.NothingRead') } $over = New-Object System.Collections.Generic.List[string] $findings = @(foreach ($row in @($rows | Where-Object { -not $_.OwnCheck -and -not $_.NotJudged })) { $gui = ($null -ne $row.Gdi -and $row.Gdi -gt $guiFail) -or ($null -ne $row.User -and $row.User -gt $guiFail) $handles = $null -ne $row.Handles -and $row.Handles -gt $handleWarn if (-not $gui -and -not $handles) { continue } $over.Add($row.Program) $value = @((Get-Text 'Value.Program.Processes') -f $row.Processes, (Format-DataSize -MB $row.PrivateMB)) if ($null -ne $row.Gdi) { $value += (Get-Text 'Value.Program.Gdi') -f $row.Gdi } if ($null -ne $row.User) { $value += (Get-Text 'Value.Program.User') -f $row.User } if ($null -ne $row.Handles) { $value += (Get-Text 'Value.Program.Handles') -f $row.Handles } if ($row.Bits) { $value += (Get-Text 'Value.Program.Bits') -f $row.Bits } if ($row.Path) { $value += $row.Path } # The worse first: at the limit of GDI or USER objects the program dies, and that # is not to be read as merely many handles. if ($gui) { New-Finding -Category Apps -Check ((Get-Text 'Check.Program.OverLimit') -f $row.Program) -Severity FAIL ` -Value ($value -join ' | ') -Meaning (Get-Text 'Meaning.Program.GuiObjects') -Hint (Get-Text 'Hint.Program.GuiObjects') } else { New-Finding -Category Apps -Check ((Get-Text 'Check.Program.OverLimit') -f $row.Program) -Severity WARN ` -Value ($value -join ' | ') -Meaning ((Get-Text 'Meaning.Program.Handles') -f $handleWarn) -Hint (Get-Text 'Hint.Program.Handles') } }) $processes = ($rows | Measure-Object -Property Processes -Sum).Sum $summary = @((Get-Text 'Value.Program.LookedAt') -f $rows.Count, $processes) if ($over.Count) { $summary += (Get-Text 'Value.Program.OverLimit') -f $over.Count, ($over -join ', ') } else { $summary += Get-Text 'Value.Program.NoneOverLimit' } $owned = @($rows | Where-Object { $_.OwnCheck }) if ($owned.Count) { $apps = @($owned | ForEach-Object { $_.OwnApp } | Select-Object -Unique) $summary += (Get-Text 'Value.Program.OwnCheck') -f $owned.Count, ($apps -join ', ') } # In the Value and not as a Meaning: an OK Finding carries none, and this has to be # said when everything is in order too. $unread = @($rows | Where-Object { $_.Unread -and -not $_.NotJudged }) if ($unread.Count) { # Whose fault it is that they are unread: nobody asked with admin rights, or # Windows does not give them to an administrator either. if ((Get-UnreadReason $Data) -eq 'Refused') { $summary += (Get-Text 'Value.Program.UnreadProtected') -f $unread.Count } else { $summary += (Get-Text 'Value.Program.Unread') -f $unread.Count } } $findings New-Finding -Category Apps -Check (Get-Text 'Check.Program.Summary') ` -Severity $(if ($over.Count) { 'INFO' } else { 'OK' }) -Value ($summary -join ' | ') ` -Hint (Get-Text 'Hint.Program.SeeFindings') } function ConvertTo-ProgramSection { <# .SYNOPSIS Every running program as a table, those in order too. Pure. .DESCRIPTION What could not be read of a program is said in the cell in one of two wordings: "nicht gelesen" where nobody read with admin rights in this Run, "nicht lesbar" where Windows did not give it with admin rights either. See Get-UnreadText. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $unread = Get-UnreadText -Reason (Get-UnreadReason $Data) $protected = (Get-UnreadReason $Data) -eq 'Refused' New-Section -Title (Get-Text 'Title.Program.All') -Row @( ConvertTo-ProgramRow -Data $Data | ForEach-Object { $note = @() if ($_.OwnCheck) { $note += (Get-Text 'Value.Program.Note.OwnCheck') -f $_.OwnCheck } if ($_.NotJudged) { $note += Get-Text 'Value.Program.Note.Kernel' } elseif ($_.Unread -and $protected) { $note += Get-Text 'Value.Program.Note.Protected' } elseif ($_.Unread) { $note += Get-Text 'Value.Program.Note.Unread' } $row = [ordered]@{} $row[(Get-Text 'Column.Program.Program')] = $_.Program $row[(Get-Text 'Column.Program.Processes')] = $_.Processes $row[(Get-Text 'Column.Program.PrivateMB')] = $_.PrivateMB $row[(Get-Text 'Column.Program.Handles')] = $(if ($null -ne $_.Handles) { $_.Handles } else { $unread }) $row[(Get-Text 'Column.Program.Gdi')] = $(if ($null -ne $_.Gdi) { $_.Gdi } else { $unread }) $row[(Get-Text 'Column.Program.User')] = $(if ($null -ne $_.User) { $_.User } else { $unread }) $row[(Get-Text 'Column.Program.Bits')] = $(if ($_.Bits) { $_.Bits } elseif ($_.Unread -and -not $_.NotJudged) { $unread } else { '' }) $row[(Get-Text 'Column.Program.Path')] = $(if ($_.Path) { $_.Path } elseif ($_.Unread -and -not $_.NotJudged) { $unread } else { '' }) $row[(Get-Text 'Column.Note')] = $note -join '; ' [pscustomobject]$row } ) } |