Reputation: 447
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
}
Upvotes: 0
Views: 791
Reputation: 969
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.
Upvotes: 1