PowerShell is an excellent scripting language for any organization and any IT Administrator. You can type a search term for anything PowerShell-related, and you will find a script probably already written that you can use as-is or tweak and use. The problem is that these scripts could contain malicious code, which might cause difficulties executing or cause issues within your environment if you do not review it. The best practice is always to check any commands, modules, and scripts before execution.

Luckily there is a tool available that performs static code analysis on modules and scripts. The tool checks the quality of PowerShell code by running against rules identified by the PowerShell Team and the community. Upon execution, it will generate a diagnostic result to inform you about potential code defects and provide possible solutions for improvements.

To get started, you first need to install and import the required PowerShell module.

Install-Module -Name PSScriptAnalyzer
Import-Module -Name PSScriptAnalyzer

With the module installed, you can now start analyzing PowerShell code. However, to understand how it works, let’s look at the analyzer rules.

Get-ScriptAnalyzerRule | Select-Object RuleName

Some of the rules available are listed below.

To look deeper at a rule, let’s look at the details for the “PSAvoidUsingEmptyCatchBlock” rule.

Get-ScriptAnalyzerRule -Name "PSAvoidUsingEmptyCatchBlock"

This time we see the “Description,” which explains the purpose of the rule.

Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks.

You can repeat this for any of the returned rules and review their purpose. However, you don’t see the actual rule details, as that is within the core module itself. The good news, though, is that you can create custom rules locally and execute against those as well as the defaults. You can filter the rules by severity and the source.

Get-ScriptAnalyzerRule -Severity Information
Get-ScriptAnalyzerRule -Severity Warning
Get-ScriptAnalyzerRule -Severity Error

Get-ScriptAnalyzerRule -SourceName PS
Get-ScriptAnalyzerRule -SourceName PSDSC

To view the actual details of a specific rule, then you can navigate into the GitHub source and view the “C#” code files.

https://github.com/PowerShell/PSScriptAnalyzer/tree/development/Rules

For example, you can use the link below to see the code behind the “PSAvoidUsingEmptyCatchBlock” rule.

https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Rules/AvoidEmptyCatchBlock.cs

The core command you will execute is “Invoke-ScriptAnalyzer,” passing in the path or definition of the PowerShell. The command evaluates script or module files (ps1, psm1, and psd1). You can choose to include or exclude rules as you need them. 

To get started, you can use some simple PowerShell, such as below.

[CmdletBinding()] 
param ( 
	[Parameter(Mandatory = $true)] 
	[String]$Password, 
	[Parameter(Mandatory = $true)] 
	[String]$Computer 
) 
 
$creds = New-Object PSCredential( 
	'user@domain.com',  
	$Password | ConvertTo-SecureString -AsPlainText -Force 
)

$creds.GetNetworkCredential().Password

Invoke-Command -Computer $Computer -ScriptBlock { Get-ChildItem C:\ } -Credential $creds

You can save the code to a PowerShell file. To perform the code analysis, you pass the path to the script.

Invoke-ScriptAnalyzer .\Sample-Script.ps1

Once analyzed, it will return any specific information, warning, and errors, based on the rules.

Invoke-ScriptAnalyzer .\Sample-Script.ps1 | `
	Select-Object Severity, Line, Message | Format-List

The analyzer identified the issue with the password not being encrypted then provided recommendations for resolving the identified problem. The resolution to this would be to change “[String]$Password” to “[SecureString]$Password” and changing “$Password | ConvertTo-SecureString -AsPlainText -Force” to this “$Password | ConvertTo-SecureString“. The newly updated code would be something like this.

[CmdletBinding()] 
param ( 
	[Parameter(Mandatory = $true)] 
	[SecureString]$Password, 
	[Parameter(Mandatory = $true)] 
	[String]$Computer 
) 
 
$creds = New-Object PSCredential( 
	'user@domain.com',  
	$Password | ConvertTo-SecureString 
)

$creds.GetNetworkCredential().Password

Invoke-Command -Computer $Computer -ScriptBlock { Get-ChildItem C:\ } -Credential $creds

Though this analysis was great, it was straightforward and only made us adjust something minor, albeit necessary. A more complex example could be something like this (provided by Microsoft).

function SuperDecrypt
{
	param($script)
	$bytes = [Convert]::FromBase64String($script)

	$xorKey = 0x42
	for($counter = 0; $counter -lt $bytes.Length; $counter++)
	{
		$bytes[$counter] = $bytes[$counter] -bxor $xorKey
	}
	
	[System.Text.Encoding]::Unicode.GetString($bytes)
}

$decrypted = SuperDecrypt "FUIwQitCNkInQm9CCkItQjFCNkJiQmVCEkI1QixCJkJlQg=="
Invoke-Expression $decrypted

With this script saved, you can now execute the analyzer.

Invoke-ScriptAnalyzer .\Sample-Script.ps1 | `
	Select-Object Severity, Line, Message | Format-List

This time it complains about the “Invoke-Expression,” a command used for legitimate and lots of malicious types of activity. The fix is to remove the “Invoke-Expression” and replace the block of code with something cleaner.

SuperDecrypt "FUIwQitCNkInQm9CCkItQjFCNkJiQmVCEkI1QixCJkJlQg==" | Write-Output

After executing the analyzer again, the only errors are for the “trailing whitespace.”

Another example could be something like this.

function Invoke-SampleFunction
{
	[CmdletBinding()]
	[OutputType([PSCustomObject[]])]
	param ()

	[PSCustomObject[]] $object = @()

	for ($i = 0; $i -lt 5; $i++)
	{
		$object += [PSCustomObject]@{
			FirstValue = "Value$i"
			SecondValue = "Value$i"
		}
	}
	return $object
}

When executed, this function simply returns values in a custom PowerShell object.

The function executes, but when run through the analyzer, it complains about an obscure error.

The error raised relates to the output returning to the pipeline from the function. By the time variable assignment happens, the current “PSCustomObject[]” unpacks and is then added dynamically to the output “Object[].”

You can simply adjust the code to return an enumerable collection with a slight adjustment in this scenario.

function Invoke-SampleFunction
{
	[CmdletBinding()]
	[OutputType([PSCustomObject[]])]
	param ()

	[PSCustomObject[]] $object = @()

	for ($i = 0; $i -lt 5; $i++)
	{
		$object += [PSCustomObject]@{
			FirstValue = "Value$i"
			SecondValue = "Value$i"
		}
	}
	Write-Output $bject -NoEnumerate
}

Another more extended example containing multiple functions and variables, along with some PowerShell aliases and malicious code, could return something like after analyzing.

DISCLAIMER: The PowerShell file linked below is a combination of commands and functions available from various places on the internet.

Sample PowerShell: https://helloitsliam.com/wp-content/uploads/2021/11/Sample-Script-1.txt

Severity : Warning
Line : 39
Message : The cmdlet ‘Find-CredFiles’ uses a plural noun. A singular
noun should be used instead.

Severity : Warning
Line : 45
Message : The cmdlet ‘Find-PasswordFiles’ uses a plural noun. A singular
noun should be used instead.

Severity : Warning
Line : 60
Message : The cmdlet ‘Find-RegPasswords’ uses a plural noun. A singular
noun should be used instead.

Severity : Warning
Line : 8
Message : ‘gci’ is an alias of ‘Get-ChildItem’. Alias can introduce
possible problems and make scripts hard to maintain. Please
consider changing alias to its full content.

Severity : Warning
Line : 14
Message : ‘%’ is an alias of ‘ForEach-Object’. Alias can introduce
possible problems and make scripts hard to maintain. Please
consider changing alias to its full content.

Severity : Warning
Line : 35
Message : ‘echo’ is an alias of ‘Write-Output’. Alias can introduce
possible problems and make scripts hard to maintain. Please
consider changing alias to its full content.

Severity : Warning
Line : 3
Message : File ‘Sample-Script.ps1’ uses Write-Host. Avoid using
Write-Host because it might not work in all hosts, does not
work when there is no host, and (prior to PS 5.0) cannot be
suppressed, captured, or redirected. Instead, use
Write-Output, Write-Verbose, or Write-Information.

Severity : Warning
Line : 26
Message : This line has a backtick at the end trailed by a whitespace
character. Did you mean for this to be a line continuation?

Luckily these are easy to resolve by adjusting the existing code. The process of cleaning up and modifying the code is a slow and laborious task but will ensure the code you execute matches best practices.