PowerShell Script Analyzer: Static Code analysis for Windows PowerShell scripts & modules

 

Introduction:

Windows Management Framework 5.0 Preview February 2015 includes the addition of a new Windows PowerShell Module called PSScriptAnalyzer. This is a static code checker for Windows PowerShell modules and scripts and is installed in $env:ProgramFiles/WindowsPowerShell/Modules.

 

 

 

PSScriptAnalyzer checks the quality of Windows PowerShell code by running a set of rules. The rules are based on PowerShell best practices identified by PowerShell Team and the community. PSScriptAnalyzer generates DiagnosticResults (errors and warnings) to inform users about potential code defects and suggests possible solutions for improvements.

PSScriptAnalyzer is shipped with a collection of built-in rules that checks various aspects of PowerShell code such as presence of uninitialized variables, usage of PSCredential Type, usage of Invoke-Expression etc. Additional functionalities such as exclude/include specific rules are also supported.

Custom rules written as PowerShell scripts can be supplied to the Script Analyzer. Read more about this in the last section. Code samples are also posted.

 

 

Usage and Scenarios:

  • PSScriptAnalyzer is shipped with the following built-in rules.

 

 

 

 

  • Excluding specified rules when using PSScriptAnalyzer

 

 

 

  • Run only a specific set of rules, by adding the IncludeRule parameter:

 

 

 

Authoring Custom/External Rules as PowerShell Scripts:

PSScriptAnalyzer can consume modules containing definitions for rules. The requirement here is that the exported function containing the rule definition must have a parameter of type “Ast” or “Token”. The engine calls into the function and supplies the Ast of the PowerShell script under analysis. The function can do the actual work of validating the Ast. Here is the usage:

By using the “CustomizedRulePath” parameter in Invoke-ScriptAnalyzer cmdlet, one can point to a folder/module containing external script rules.

 

 

Example 1 – Rule to detect the presence of Write-Host:

<#

.SYNOPSIS

    You should never use Write-Host to create any script output whatsoever.

.DESCRIPTION

    It is generally accepted that you should never use Write-Host to create any script output whatsoever, unless your script (or function, or whatever) uses the Show verb (as in, Show-Performance).

    That verb explicitly means “show on the screen, with no other possibilities.” Like Show-Command.

    To fix a violation of this rule, please replace Write-Host with Write-Output in most scenarios.

.EXAMPLE

    Test-WriteHost -CommandAst $CommandAst

.INPUTS

    [System.Management.Automation.Language.CommandAst]

.OUTPUTS

    [PSCustomObject[]]

.NOTES

    Reference: Output, The Community Book of PowerShell Practices.

#>

function Test-WriteHost

{

    [CmdletBinding()]

    [OutputType([PSCustomObject[]])]

    Param

    (

        [Parameter(Mandatory = $true)]

        [ValidateNotNullOrEmpty()]

        [System.Management.Automation.Language.CommandAst]

        $CommandAst

    )

 

    Process

    {

        try

        {

            $commandName = $CommandAst.GetCommandName()

            # Checks command name, if the command name matches Write-Host or

            # user-defined aliases, this rule is triggered.

 

            if ($null -ne $commandName)

            {

                $alias = (Get-Alias -Definition “Write-Host” -ErrorAction SilentlyContinue).Name

 

                if (($commandName -eq “Write-Host”) -or

                    ($alias -eq $commandName))

                {

                    [PSCustomObject]@{Message  = “Avoid Using Write-Host”;

                                      Extent   = $CommandAst.Extent;

                                      RuleName = $PSCmdlet.MyInvocation.InvocationName;

                                      Severity = “Warning”}                   

                }

            }

        }

        catch

        {

            $PSCmdlet.ThrowTerminatingError($PSItem)

        }

    }

}

 

  

 

Example 2 – Rule for Try-Catch-Finally for handling terminating errors:

<#

.SYNOPSIS

    Considers to use try-catch-finally statements instead of using $?.

.DESCRIPTION

    When you need to examine the error that occurred, try to avoid using $?. It actually doesn’t mean an error did or did not occur; it’s reporting whether or not the last-run command considered itself to have completed successfully.

    You get no details on what happened. To fix a violation of this rule, please consider to use try-catch-finally statements.

.EXAMPLE

    Test-QuestionVariable -ScriptBlockAst $ScriptBlockAst

.INPUTS

    [System.Management.Automation.Language.ScriptBlockAst]

.OUTPUTS

    [PSCustomObject[]]

.NOTES

    Reference: Trapping and Capturing Errors, Windows PowerShell Best Practices.

#>

function Test-QuestionVariable

{

    [CmdletBinding()]

    [OutputType([PSCustomObject[]])]

    Param

    (

        [Parameter(Mandatory = $true)]

        [ValidateNotNullOrEmpty()]

        [System.Management.Automation.Language.ScriptBlockAst]

        $ScriptBlockAst

    )

 

    Process

    {

        try

        {

            # Gets question variables

            $questionVariables = $ScriptBlockAst.FindAll(

                {$args[0] –is [System.Management.Automation.Language.VariableExpressionAst] –and  

                $args[0].VariablePath.UserPath -eq ’?’},$true)

 

            foreach ($questionVariable in $questionVariables)

            {

                [PSCustomObject]@{Message  = “Avoid Using Question Variable”;

                                  Extent   = $questionVariable.Extent;

                                  RuleName = $PSCmdlet.MyInvocation.InvocationName;

                                  Severity = “Warning”}

            }

        }

        catch

        {

            $PSCmdlet.ThrowTerminatingError($PSItem)

        }

    }

}

 

 

Note that in the next release we will be moving to using a specific type (called DiagnosticRecord), instead of using PSCustomObject, when returning results of the rule.

Also, the next blog post will talk about authoring rules in C#

That’s it for now! Run this framework against your scripts/modules and let us know of your feedback/suggestions.

 

Feedback and Contact:

https://connect.microsoft.com/PowerShell/Feedback

You can also follow us @: http://blogs.msdn.com/b/powershell/

 

 Thanks!

Raghu Shantha [MSFT]

PowerShell ScriptAnalyzer Team