0

Looking for advice on error handling in Powershell. I think I understand the concept behind using Try/Catch but I'm struggling on where to utilize this in my scripts or how granular I need to be.

For example, should I use the try/catch inside my functions and if so, should I insert the actions of my function inside the try or do I need to break it

down further? OR, should I try to handle the error when I call my function? Doing something like this:

Try{
     Get-MyFunction 
    } catch{ Do Something" 
} 

Here's an example of a script I wrote which is checking for some indicators of compromise on a device. I have an application that will launch this script and capture the final output. The application requires the final output to be in the following format so any failure should generate this.

[output]
result=<0 or 1>
msg= <string>

Which I'm doing like this:

Write-Host "[output]"
Write-Host "result=0"
Write-Host "msg = $VariableContainingOutput -NoNewline

Two of my functions create custom objects and then combine these for the final output so I'd like to capture any errors in this same format. If one function generates an error, it should record these and continue.

If I just run the code by itself (not using function) this works but with the function my errors are not captured.

This needs to work on PowerShell 2 and up. The Add-RegMember and Get-RegValue functions called by this script are not shown.

function Get-ChangedRunKey {
    [CmdletBinding()]
    param()
    process 
    {
        $days = '-365' 
        $Run = @()
        $AutoRunOutput = @()
        $RunKeyValues = @("HKLM:\Software\Microsoft\Windows\CurrentVersion\Run", 
                      "HKLM:\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run", 
                      "HKU:\S-1-5-21-*\Software\Microsoft\Windows\CurrentVersion\Run",
                      "HKU:\S-1-5-21-*\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run"
                      )
        Try{
            $Run += $RunKeyValues | 
            ForEach-Object {
                Get-Item $_ -ErrorAction SilentlyContinue | 
                Add-RegKeyMember -ErrorAction SilentlyContinue | 
                Where-Object {
                    $_.lastwritetime -gt (Get-Date).AddDays($days)
                } | 
                Select-Object Name,LastWriteTime,property
            } 
            if ($Run -ne $Null)
            {
                $AutoRunPath = ( $Run | 
                    ForEach-Object {
                        $_.name
                    }
                ) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
                $AutoRunValue = $AutoRunPath  | 
                    Where-Object { 
                        $_ -and $_.Trim() 
                    } |
                    ForEach-Object {
                        Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
                    }
            }
        #Build Custom Object if modified Run keys are found 
            if($AutorunValue -ne $null)
            {
                foreach ($Value in $AutoRunValue) {
                    $AutoRunOutput += New-Object PSObject -Property @{
                        Description = "Autorun"
                        path = $Value.path
                        value = $Value.value
                    }
                }
            }
            Write-Output $AutoRunOutput
        }catch{
                $AutoRunOutput += New-Object PSObject -Property @{
                    Description = "Autorun"
                    path = "N/A"
                    value = "Error accessing Autorun data. $($Error[0])"
                }
        }
    }
}
function Get-ShellIOC {
    [CmdletBinding()]
    param()
    process 
    {
        $ShellIOCOutput = @()
        $ShellIOCPath = 'HKU:\' + '*' + '_Classes\*\shell\open\command'
     Try{
            $ShellIOCValue = (Get-Item $ShellIOCPath -ErrorAction SilentlyContinue | 
                Select-Object name,property | 
                ForEach-Object {
                    $_.name
                }
            ) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:" 
            $ShellIOCDetected = $ShellIOCValue | 
                ForEach-Object {
                    Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
                } | 
                Where-Object {
                    $_.value -like "*cmd.exe*" -or 
                    $_.value -like "*mshta.exe*"
                }
            if($ShellIOCDetected -ne $null) 
            { 
                foreach ($ShellIOC in $ShellIOCDetected) {
                    $ShellIOCOutput += New-Object PSObject -Property @{
                        Description = "Shell_IOC_Detected"
                        path = $ShellIOC.path
                        value = $ShellIOC.value
                    }
                }
            }
            Write-Output $ShellIOCOutput
        }catch{
                $ShellIOCOutput += New-Object PSObject -Property @{
                    Description = "Shell_IOC_Detected"
                    path = "N/A"
                    value = "Error accessing ShellIOC data. $($Error[0])"
                    }
        }
    }
}
function Set-OutputFormat {
    [CmdletBinding()]
    param()
    process 
    {   
        $FormattedOutput = $AutoRunOutput + $ShellIOCOutput | 
        ForEach-Object {
            "Description:" + $_.description + ',' + "Path:" + $_.path + ',' + "Value:" + $_.value + "|"
        }
        Write-Output $FormattedOutput 
    }
}

if (!(Test-Path "HKU:\")){
    try{
        New-PSDrive -PSProvider Registry -Root HKEY_USERS -Name HKU -ErrorAction Stop | Out-Null
    }catch{
        Write-Output "[output]"
        Write-Output "result=0"
        Write-Host "msg = Unable to Connect HKU drive" -NoNewline
    }
}  
$AutoRunOutput = Get-ChangedRunKey

$ShellIOCOutput = Get-ShellIOC 

$FormattedOutput = Set-OutputFormat 

Write-Output "[output]"

if ($FormattedOutput -eq $Null)
{
    Write-Output "result=0"
    Write-Host "msg= No Items Detected" -NoNewline
}
else
{
    Write-Output "result=1"
    Write-Host "msg=Items Detected: $($FormattedOutput)" -NoNewline
}
7
  • 1
    First, if you use Write-Host in the function, that output will never be captured when you call the function and assign the output to a variable. Use Write-Output if you want output that you can capture. Second, the portion of this question that has to do with ideas about when/where/how to implement error handling fall into the Primarily Opinion Based category. Commented Mar 14, 2018 at 18:21
  • Ok, let me take another look at that. I was using Write-Host because Write-Output doesn't have the -nonewline parameter. This application doesn't allow line breaks in the output which was occurring when I was exporting the final combined custom object. I can likely remove it from within the functions though. As to your other point, I'm not looking for a right/wrong way, just what is best practice. Everything I've read just covers the documentation of WHAT to do but not WHEN to do it. Commented Mar 14, 2018 at 18:35
  • WHEN to do it is opinion. Commented Mar 14, 2018 at 18:37
  • 1
    Also, you assign the output of the function to a variable. In the catch code you never actually output anything. So nothing will be assigned to the variable. Commented Mar 14, 2018 at 18:38
  • OK I see that now. Let me update. So in my earlier comment I was mistaken. I was thinking I had a write-host within the functions, but it's not. Commented Mar 14, 2018 at 18:41

1 Answer 1

1

You have to know that there are 2 error types in PowerShell:

  • Terminating Errors: Those get caught automatically in the catch block

  • Non-Terminating Error: If you want to catch them then the command in question needs to be execution using -ErrorAction Stop. If it is not a PowerShell command but an executable, then you need to check stuff like the exit code or $?. Therefore I suggest wrapping your entire action in an advanced function on which you then call using -ErrorAction Stop.

Apart from that I would like to remark that PowerShell version 2 has already been deprecated. The reason for why non-terminating errors exists is because there are cases like for example processing multiple objects from the pipeline where you might not want it to stop just because it did not work for one object. And please do not use Write-Host, use Write-Verbose or Write-Output depending on the use case.

Sign up to request clarification or add additional context in comments.

4 Comments

Meh, I use Write-Host all the time. Any output that is just informational and I don't care to capture. I essentially use it to make sure long running processes don't hang. It's way easier than a progress bar.
Well, but others might not want to see output on the host. That is the main problem with Write-Host and why Write-Verbose should be used since the verbose stream can be redirected. From the horse's mouth: jsnover.com/blog/2013/12/07/write-host-considered-harmful
Aaah...I will fully support the statement 'Please do not use Write-Host, use Write-Verbose or Write-Output when writing code that you will share with others.' I do all kinds of things differently when cleaning it up for someone elses consumption as opposed to a quick one off to do some task right now for me.
Also from that article: "I often use Write-Host when I’m writing a throw away script or function."

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.