Edit

Creating custom rules

PSScriptAnalyzer uses the Managed Extensibility Framework (MEF) to import all rules defined in the assembly. It can also consume rules written in PowerShell scripts.

Users can specify custom rules using the CustomizedRulePath parameter of the Invoke-ScriptAnalyzer cmdlet.

This article provides a basic guide for creating your own customized rules.

Basic requirements

Functions should have comment-based help

Include the .DESCRIPTION field. This field becomes the description for the customized rule.

<#
.SYNOPSIS
    Name of your rule.
.DESCRIPTION
    This would be the description of your rule. Please refer to Rule Documentation
    for consistent rule messages.
.EXAMPLE
.INPUTS
.OUTPUTS
.NOTES
#>

Output type should be an array of DiagnosticRecord objects

[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]

Each function must have a Token array or an Ast parameter

The name of the Ast parameter name must end with Ast.

Param
(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [System.Management.Automation.Language.ScriptBlockAst]
    $testAst
)

The name of the Token parameter name must end with Token.

Param
(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [System.Management.Automation.Language.Token[]]
    $testToken
)

DiagnosticRecord should have the required properties

The DiagnosticRecord should have at least four properties:

  • Message
  • Extent
  • RuleName
  • Severity
$result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{
    Message  = 'This is a sample rule'
    Extent   = $ast.Extent
    RuleName = $PSCmdlet.MyInvocation.InvocationName
    Severity = 'Warning'
}

Since version 1.17.0, you can include a SuggestedCorrections property of type IEnumerable<CorrectionExtent>. Make sure to specify the correct type. For example:

$startLineNumber =  $ast.Extent.StartLineNumber
$endLineNumber = $ast.Extent.EndLineNumber
$startColumnNumber = $ast.Extent.StartColumnNumber
$endColumnNumber = $ast.Extent.EndColumnNumber
$correction = 'Correct text that replaces Extent text'
$file = $MyInvocation.MyCommand.Definition
$optionalDescription = 'Useful but optional description text'
$objParams = @{
  TypeName = 'Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent'
  ArgumentList = $startLineNumber, $endLineNumber, $startColumnNumber,
                 $endColumnNumber, $correction, $file, $optionalDescription
}
$correctionExtent = New-Object @objParams
$suggestedCorrections = New-Object System.Collections.ObjectModel.Collection[$($objParams.TypeName)]
$suggestedCorrections.add($correctionExtent) | Out-Null

[Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord]@{
    Message              = 'This is a rule with a suggested correction'
    Extent               = $ast.Extent
    RuleName             = $PSCmdlet.MyInvocation.InvocationName
    Severity             = 'Warning'
    RuleSuppressionID    = 'MyRuleSuppressionID'
    SuggestedCorrections = $suggestedCorrections
}

Make sure you export the function

You must export the functions you create so that PSScriptAnalyzer can find them.

Export-ModuleMember -Function (FunctionName)

Example rule function

<#
    .SYNOPSIS
    Uses #Requires -RunAsAdministrator instead of your own methods.
    .DESCRIPTION
    The #Requires statement prevents a script from running unless the Windows PowerShell
    version, modules, snap-ins, and module and snap-in version prerequisites are met.
    Since Windows PowerShell 4.0, the #Requires statement lets script developers require that
    sessions be run with elevated user rights (run as Administrator). Script developers do
    not need to write their own methods any more. To fix a violation of this rule, please
    consider using #Requires -RunAsAdministrator instead of your own methods.
    .EXAMPLE
    Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst
    .INPUTS
    [System.Management.Automation.Language.ScriptBlockAst]
    .OUTPUTS
    [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
    .NOTES
    None
#>
function Measure-RequiresRunAsAdministrator {
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]
        $ScriptBlockAst
    )

    begin {
        $MeasureRequiresAdmin = @(
            'The #Requires statement prevents a script from running unless the PowerShell version,'
            'modules, snap-ins, and module and snap-in version prerequisites are met. Since'
            'Windows PowerShell 4.0, the #Requires statement lets script developers require that'
            'sessions be run with elevated user rights (run as Administrator). Script developers'
            'don''t need to write their own methods to test for elevated rights. To fix a violation'
            'of this rule, use #Requires -RunAsAdministrator instead of your own methods.'
        ) -join ' '

        # Finds specific method, IsInRole.
        [ScriptBlock]$predicate = {
            param($Ast)
            return $Ast.Member.Value -eq 'IsInRole'
        }
    }

    process {
        # Exit early if the script block has a #Requires -RunAsAdministrator statement.
        if ($ScriptBlockAst.ScriptRequirements.IsElevationRequired) {
          return
        }

        # Test for calls to IsInRole() method
        [System.Management.Automation.Language.Ast]$methodAst = $ScriptBlockAst.Find($predicate, $true)
        if ($methodAst) {
            [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{
                Message  = $MeasureRequiresAdmin
                Extent   = $methodAst.Extent
                RuleName = $PSCmdlet.MyInvocation.InvocationName
                Severity = 'Information'
            }
        }
    }
}

More examples can be found in the CommunityAnalyzerRules folder on GitHub.