Private/Kinds/Startup.ps1
|
# The Startup Kind: what runs before the Technician's Customer gets to work. # # The Finding is a count, which is all a first-level Technician needs to act on. The # Section beneath it is the list, which is what second level needs to say which entry to # remove - and which is why the list travels with the Report rather than being something # to go back to the machine for. function Get-StartupData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $entries = @() $available = $true try { $entries = @(Get-CimInstance Win32_StartupCommand -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)" Command = "$($_.Command)" Location = "$($_.Location)" User = "$($_.User)" } }) } catch { $available = $false } $tasks = Get-StartupScheduledTask [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Startup' Available = $available Entries = $entries # $null where Windows has no way to list them, which is not the same as none. TasksRead = $null -ne $tasks Tasks = @($tasks) } } function Get-StartupScheduledTask { <# .SYNOPSIS The scheduled tasks that are not Windows' own, with when each last ran and how that ended. $null where they cannot be listed. .DESCRIPTION Windows ships some two hundred tasks under \Microsoft\, and a list of those hides the dozen somebody put on this machine. Without admin rights Windows hands out the tasks this user may see and says nothing of the rest. #> [CmdletBinding()] [OutputType([psobject])] param() if (-not (Get-Command -Name Get-ScheduledTask -ErrorAction SilentlyContinue)) { return $null } $found = New-Object System.Collections.Generic.List[object] foreach ($task in @(Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { "$($_.TaskPath)" -notlike '\Microsoft\*' })) { $info = $null try { $info = $task | Get-ScheduledTaskInfo -ErrorAction Stop } catch { } $account = "$($task.Principal.UserId)" if (-not $account) { $account = "$($task.Principal.GroupId)" } $found.Add([pscustomobject]@{ Name = "$($task.TaskName)" Folder = "$($task.TaskPath)" State = "$($task.State)" Account = $account Triggers = @($task.Triggers | ForEach-Object { "$($_.CimClass.CimClassName)" }) Actions = @($task.Actions | ForEach-Object { ("{0} {1}" -f $_.Execute, $_.Arguments).Trim() }) LastRun = $(if ($info) { $info.LastRunTime }) LastResult = $(if ($info) { $info.LastTaskResult }) NextRun = $(if ($info) { $info.NextRunTime }) }) } , $found.ToArray() } function Test-StartupTaskFailed { <# .SYNOPSIS Whether a task's last run ended badly. Pure. .DESCRIPTION A result is not a failure for being other than zero: 0x41300 to 0x41305 and 0x41325 say the task is ready, running, disabled, has not run yet or is queued. A task that is switched off is nobody's problem, whatever its last run was. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$Task) if ("$(Get-DataProperty $Task 'State')" -eq 'Disabled') { return $false } $result = ConvertTo-Number (Get-DataProperty $Task 'LastResult') if ($null -eq $result -or $result -eq 0) { return $false } if (($result -ge 0x41300 -and $result -le 0x41305) -or $result -eq 0x41325) { return $false } $true } function ConvertTo-StartupTaskRow { <# .SYNOPSIS One scheduled task as a row of the Section, in a Technician's words. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Task) $state = switch ("$(Get-DataProperty $Task 'State')") { 'Ready' { Get-Text 'Value.Startup.TaskState.Ready' } 'Disabled' { Get-Text 'Value.Startup.TaskState.Disabled' } 'Running' { Get-Text 'Value.Startup.TaskState.Running' } 'Queued' { Get-Text 'Value.Startup.TaskState.Queued' } default { "$_" } } $triggers = @((Get-DataCollection $Task 'Triggers') | ForEach-Object { switch -Regex ("$_") { 'Daily' { Get-Text 'Value.Startup.TaskTrigger.Daily'; break } 'Weekly' { Get-Text 'Value.Startup.TaskTrigger.Weekly'; break } 'Logon' { Get-Text 'Value.Startup.TaskTrigger.Logon'; break } 'Boot' { Get-Text 'Value.Startup.TaskTrigger.Boot'; break } 'Idle' { Get-Text 'Value.Startup.TaskTrigger.Idle'; break } 'Event' { Get-Text 'Value.Startup.TaskTrigger.Event'; break } 'Registration' { Get-Text 'Value.Startup.TaskTrigger.Registration'; break } 'SessionState' { Get-Text 'Value.Startup.TaskTrigger.Session'; break } 'Time' { Get-Text 'Value.Startup.TaskTrigger.Time'; break } default { Get-Text 'Value.Startup.TaskTrigger.Other' } } } | Select-Object -Unique) if (-not $triggers.Count) { $triggers = @(Get-Text 'Value.Startup.TaskTrigger.None') } $actions = @((Get-DataCollection $Task 'Actions') | ForEach-Object { if ("$_".Trim()) { "$_" } else { Get-Text 'Value.Startup.TaskAction.Handler' } }) # Windows gives a task that never ran a date in 1999. $time = { param($value) $at = $value -as [datetime] if ($at -and $at.Year -gt 2000) { (Get-Text 'Value.Startup.TaskTime') -f $at } else { '' } } $lastRun = & $time (Get-DataProperty $Task 'LastRun') $number = ConvertTo-Number (Get-DataProperty $Task 'LastResult') $result = '' if ($null -ne $number) { $hex = '{0:X}' -f [int64]$number $result = if ($number -eq 0 -and $lastRun) { Get-Text 'Value.Startup.TaskResult.Success' } elseif ($number -eq 0x41303 -or -not $lastRun) { Get-Text 'Value.Startup.TaskResult.NeverRun' } elseif ($number -eq 0x41301) { Get-Text 'Value.Startup.TaskResult.Running' } elseif (Test-StartupTaskFailed -Task $Task) { (Get-Text 'Value.Startup.TaskResult.Failed') -f $hex } else { (Get-Text 'Value.Startup.TaskResult.Other') -f $hex } } $row = [ordered]@{} $row[(Get-Text 'Column.Startup.Task')] = "$(Get-DataProperty $Task 'Name')" $row[(Get-Text 'Column.Startup.TaskFolder')] = "$(Get-DataProperty $Task 'Folder')" $row[(Get-Text 'Column.Startup.TaskState')] = $state $row[(Get-Text 'Column.Startup.TaskAccount')] = "$(Get-DataProperty $Task 'Account')" $row[(Get-Text 'Column.Startup.TaskTrigger')] = $triggers -join ', ' $row[(Get-Text 'Column.Startup.TaskAction')] = $actions -join ' | ' $row[(Get-Text 'Column.Startup.TaskLastRun')] = $lastRun $row[(Get-Text 'Column.Startup.TaskResult')] = $result $row[(Get-Text 'Column.Startup.TaskNextRun')] = & $time (Get-DataProperty $Task 'NextRun') [pscustomobject]$row } function ConvertTo-StartupFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) $warnAbove = Get-Parameter $Parameters 'StartupEntryWarnAbove' 15 $failAbove = Get-Parameter $Parameters 'StartupEntryFailAbove' ([double]::MaxValue) if (-not (Get-DataProperty $Data 'Available')) { return New-UnavailableFinding -Category Startup -Check (Get-Text 'Check.Startup.StartupEntries') ` -Hint (Get-Text 'Hint.Startup.WindowsDidNotEnumerateIts') } $entries = Get-DataCollection $Data 'Entries' New-Finding -Category Startup -Check (Get-Text 'Check.Startup.StartupEntries') ` -Severity (Get-Severity $entries.Count $warnAbove $failAbove) -Value $entries.Count ` -Hint (Get-Text 'Hint.Startup.ManyAutostartProgramsReviewIn') # How many there are, and which ended badly: the list is the Section. No verdict - a # task that failed last night may be a laptop that was not at the office. if (Get-DataProperty $Data 'TasksRead') { $tasks = Get-DataCollection $Data 'Tasks' $failed = @($tasks | Where-Object { Test-StartupTaskFailed -Task $_ } | ForEach-Object { "$(Get-DataProperty $_ 'Name')" }) if ($failed.Count) { New-Finding -Category Startup -Check (Get-Text 'Check.Startup.ScheduledTasks') -Severity INFO ` -Value ((Get-Text 'Value.Startup.TasksFailed') -f $tasks.Count, ($failed -join ', ')) ` -Hint (Get-Text 'Hint.Startup.TasksFailed') } else { New-Finding -Category Startup -Check (Get-Text 'Check.Startup.ScheduledTasks') -Severity INFO ` -Value ((Get-Text 'Value.Startup.Tasks') -f $tasks.Count) } } } function ConvertTo-StartupSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) New-Section -Title (Get-Text 'Title.Startup.StartupEntries') -Row @( (Get-DataCollection $Data 'Entries') | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Startup.Entry')] = Get-DataProperty $_ 'Name' $row[(Get-Text 'Column.Startup.Command')] = Get-DataProperty $_ 'Command' # A registry key or a startup folder, as Windows names it. $row[(Get-Text 'Column.Startup.Location')] = Get-DataProperty $_ 'Location' $row[(Get-Text 'Column.Startup.User')] = Get-DataProperty $_ 'User' [pscustomobject]$row } ) $tasks = Get-DataCollection $Data 'Tasks' if ($tasks.Count) { New-Section -Title (Get-Text 'Title.Startup.ScheduledTasks') -Row @($tasks | ForEach-Object { ConvertTo-StartupTaskRow -Task $_ }) } } |