From 2aca47458d8c79fb055ba0776df5e128c6950634 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 23 Jun 2026 10:35:48 -0500 Subject: [PATCH 1/2] Update example based on UUF comments --- .../PSScriptAnalyzer/create-custom-rule.md | 138 ++++++++---------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md index 748d200b..f3f60f92 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md @@ -33,7 +33,7 @@ Include the `.DESCRIPTION` field. This field becomes the description for the cus #> ``` -### Output type should be **DiagnosticRecord** +### Output type should be an array of **DiagnosticRecord** objects ```powershell [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] @@ -41,7 +41,7 @@ Include the `.DESCRIPTION` field. This field becomes the description for the cus ### Each function must have a Token array or an Ast parameter -The name of the **Ast** parameter name must end with **Ast**. +The name of the **Ast** parameter name must end with `Ast`. ```powershell Param @@ -53,7 +53,7 @@ Param ) ``` -The name of the **Token** parameter name must end with **Token**. +The name of the **Token** parameter name must end with `Token`. ```powershell Param @@ -108,7 +108,6 @@ $suggestedCorrections.add($correctionExtent) | Out-Null "Extent" = $ast.Extent "RuleName" = $PSCmdlet.MyInvocation.InvocationName "Severity" = "Warning" - "Severity" = "Warning" "RuleSuppressionID" = "MyRuleSuppressionID" "SuggestedCorrections" = $suggestedCorrections } @@ -132,7 +131,7 @@ Export-ModuleMember -Function (FunctionName) 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. From Windows PowerShell 4.0, the #Requires statement let script developers require that - sessions be run with elevated user rights (run as Administrator). Script developers does + 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 @@ -144,11 +143,10 @@ Export-ModuleMember -Function (FunctionName) .NOTES None #> -function Measure-RequiresRunAsAdministrator -{ +function Measure-RequiresRunAsAdministrator { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] - Param + param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -156,83 +154,71 @@ function Measure-RequiresRunAsAdministrator $ScriptBlockAst ) - Process - { - $results = @() - try - { - #region Define predicates to find ASTs. - # Finds specific method, IsInRole. - [ScriptBlock]$predicate1 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) - { - [System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast - if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) - { - [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member - if ($sceAst.Value -eq 'isinrole') - { - $returnValue = $true - } - } - } - return $returnValue - } - - # Finds specific value, [system.security.principal.windowsbuiltinrole]::administrator. - [ScriptBlock]$predicate2 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) - { - [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast - if ($asAst.Right.ToString() -eq '[system.security.principal.windowsbuiltinrole]::administrator') - { + 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]$predicate1 = { + param ([System.Management.Automation.Language.Ast]$Ast) + [bool]$returnValue = $false + if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) { + [System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast + if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { + [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member + if ($sceAst.Value -eq 'IsInRole') { $returnValue = $true } } - return $returnValue } - #endregion - #region Finds ASTs that match the predicates. - - [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) - [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) - if ($null -ne $ScriptBlockAst.ScriptRequirements) - { - if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and - ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) - { - $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $Messages.MeasureRequiresRunAsAdministrator - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' - } - $results += $result + return $returnValue + } + + # Finds specific value, [System.Security.Principal.WindowsBuiltInRole]::Administrator. + [ScriptBlock]$predicate2 = { + param ([System.Management.Automation.Language.Ast]$Ast) + [bool]$returnValue = $false + if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { + [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast + if ($asAst.Right.ToString() -eq '[System.Security.Principal.WindowsBuiltInRole]::Administrator') { + $returnValue = $true } } - else - { - if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) - { - $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $Messages.MeasureRequiresRunAsAdministrator - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' - } - $results += $result + return $returnValue + } + } + + process { + # Find ASTs that match the predicates. + [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) + [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) + + if ($null -ne $ScriptBlockAst.ScriptRequirements) { + if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and + ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { + [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + 'Message' = $MeasureRequiresAdmin + 'Extent' = $assignmentAst.Extent + 'RuleName' = $PSCmdlet.MyInvocation.InvocationName + 'Severity' = 'Information' } } - return $results - #endregion + return } - catch - { - $PSCmdlet.ThrowTerminatingError($PSItem) + + if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { + [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + 'Message' = $MeasureRequiresAdmin + 'Extent' = $assignmentAst.Extent + 'RuleName' = $PSCmdlet.MyInvocation.InvocationName + 'Severity' = 'Information' + } } } } From be1c85e00eb4ba4139eda4addab40faa505b59f7 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 23 Jun 2026 11:12:00 -0500 Subject: [PATCH 2/2] Simplify example --- .../PSScriptAnalyzer/create-custom-rule.md | 93 ++++++------------- 1 file changed, 30 insertions(+), 63 deletions(-) diff --git a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md index f3f60f92..1e2a21c1 100644 --- a/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md +++ b/reference/docs-conceptual/PSScriptAnalyzer/create-custom-rule.md @@ -76,10 +76,10 @@ The **DiagnosticRecord** should have at least four properties: ```powershell $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{ - "Message" = "This is a sample rule" - "Extent" = $ast.Extent - "RuleName" = $PSCmdlet.MyInvocation.InvocationName - "Severity" = "Warning" + Message = 'This is a sample rule' + Extent = $ast.Extent + RuleName = $PSCmdlet.MyInvocation.InvocationName + Severity = 'Warning' } ``` @@ -87,13 +87,13 @@ Since version 1.17.0, you can include a **SuggestedCorrections** property of typ **IEnumerable\**. Make sure to specify the correct type. For example: ```powershell -[int]$startLineNumber = $ast.Extent.StartLineNumber -[int]$endLineNumber = $ast.Extent.EndLineNumber -[int]$startColumnNumber = $ast.Extent.StartColumnNumber -[int]$endColumnNumber = $ast.Extent.EndColumnNumber -[string]$correction = 'Correct text that replaces Extent text' -[string]$file = $MyInvocation.MyCommand.Definition -[string]$optionalDescription = 'Useful but optional description text' +$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, @@ -104,12 +104,12 @@ $suggestedCorrections = New-Object System.Collections.ObjectModel.Collection[$($ $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 + Message = 'This is a rule with a suggested correction' + Extent = $ast.Extent + RuleName = $PSCmdlet.MyInvocation.InvocationName + Severity = 'Warning' + RuleSuppressionID = 'MyRuleSuppressionID' + SuggestedCorrections = $suggestedCorrections } ``` @@ -165,59 +165,26 @@ function Measure-RequiresRunAsAdministrator { ) -join ' ' # Finds specific method, IsInRole. - [ScriptBlock]$predicate1 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) { - [System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast - if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { - [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member - if ($sceAst.Value -eq 'IsInRole') { - $returnValue = $true - } - } - } - return $returnValue - } - - # Finds specific value, [System.Security.Principal.WindowsBuiltInRole]::Administrator. - [ScriptBlock]$predicate2 = { - param ([System.Management.Automation.Language.Ast]$Ast) - [bool]$returnValue = $false - if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { - [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast - if ($asAst.Right.ToString() -eq '[System.Security.Principal.WindowsBuiltInRole]::Administrator') { - $returnValue = $true - } - } - return $returnValue + [ScriptBlock]$predicate = { + param($Ast) + return $Ast.Member.Value -eq 'IsInRole' } } process { - # Find ASTs that match the predicates. - [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) - [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) - - if ($null -ne $ScriptBlockAst.ScriptRequirements) { - if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and - ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { - [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ - 'Message' = $MeasureRequiresAdmin - 'Extent' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' - } - } - return + # Exit early if the script block has a #Requires -RunAsAdministrator statement. + if ($ScriptBlockAst.ScriptRequirements.IsElevationRequired) { + return } - if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) { + # 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' = $assignmentAst.Extent - 'RuleName' = $PSCmdlet.MyInvocation.InvocationName - 'Severity' = 'Information' + Message = $MeasureRequiresAdmin + Extent = $methodAst.Extent + RuleName = $PSCmdlet.MyInvocation.InvocationName + Severity = 'Information' } } }