Skip to content

Commit 278c15a

Browse files
Fixes #38 - Update overview & migrate 2 articles from source (#39)
* Update overview & migrate 2 articles from source * Apply suggestions from code review Co-authored-by: Michael T Lombardi (He/Him) <[email protected]> * Fix broken links * Apply suggestions from code review Co-authored-by: Michael T Lombardi (He/Him) <[email protected]>
1 parent b550563 commit 278c15a

File tree

5 files changed

+791
-320
lines changed

5 files changed

+791
-320
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
description: This article provides a basic guide for creating your own customized rules.
3+
ms.date: 03/22/2022
4+
title: Creating custom rules
5+
---
6+
# Creating custom rules
7+
8+
PSScriptAnalyzer uses the [Managed Extensibility Framework (MEF)](/dotnet/framework/mef/) to import
9+
all rules defined in the assembly. It can also consume rules written in PowerShell scripts.
10+
11+
When calling `Invoke-ScriptAnalyzer`, users can specify custom rules using the
12+
**CustomizedRulePath** parameter.
13+
14+
This article provides a basic guide for creating your own customized rules.
15+
16+
## Basic requirements
17+
18+
### Functions should have comment-based help
19+
20+
Include the `.DESCRIPTION` field. This becomes the description for the customized rule.
21+
22+
```powershell
23+
<#
24+
.SYNOPSIS
25+
Name of your rule.
26+
.DESCRIPTION
27+
This would be the description of your rule. Please refer to Rule Documentation
28+
for consistent rule messages.
29+
.EXAMPLE
30+
.INPUTS
31+
.OUTPUTS
32+
.NOTES
33+
#>
34+
```
35+
36+
### Output type should be **DiagnosticRecord**
37+
38+
```powershell
39+
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
40+
```
41+
42+
### Each function must have a Token array or an Ast parameter
43+
44+
The name of the **Ast** parameter name must end with **Ast**.
45+
46+
```powershell
47+
Param
48+
(
49+
[Parameter(Mandatory = $true)]
50+
[ValidateNotNullOrEmpty()]
51+
[System.Management.Automation.Language.ScriptBlockAst]
52+
$testAst
53+
)
54+
```
55+
56+
The name of the **Token** parameter name must end with **Token**.
57+
58+
```powershell
59+
Param
60+
(
61+
[Parameter(Mandatory = $true)]
62+
[ValidateNotNullOrEmpty()]
63+
[System.Management.Automation.Language.Token[]]
64+
$testToken
65+
)
66+
```
67+
68+
### DiagnosticRecord should have the required properties
69+
70+
The **DiagnosticRecord** should have at least four properties:
71+
72+
- **Message**
73+
- **Extent**
74+
- **RuleName**
75+
- **Severity**
76+
77+
```powershell
78+
$result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{
79+
"Message" = "This is a sample rule"
80+
"Extent" = $ast.Extent
81+
"RuleName" = $PSCmdlet.MyInvocation.InvocationName
82+
"Severity" = "Warning"
83+
}
84+
```
85+
86+
Since version 1.17.0, you can include a **SuggestedCorrections** property of type
87+
**IEnumerable\<CorrectionExtent\>**. Make sure to specify the correct type. For example:
88+
89+
```powershell
90+
[int]$startLineNumber = $ast.Extent.StartLineNumber
91+
[int]$endLineNumber = $ast.Extent.EndLineNumber
92+
[int]$startColumnNumber = $ast.Extent.StartColumnNumber
93+
[int]$endColumnNumber = $ast.Extent.EndColumnNumber
94+
[string]$correction = 'Correct text that replaces Extent text'
95+
[string]$file = $MyInvocation.MyCommand.Definition
96+
[string]$optionalDescription = 'Useful but optional description text'
97+
$objParams = @{
98+
TypeName = 'Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent'
99+
ArgumentList = $startLineNumber, $endLineNumber, $startColumnNumber,
100+
$endColumnNumber, $correction, $optionalDescription
101+
}
102+
$correctionExtent = New-Object @objParams
103+
$suggestedCorrections = New-Object System.Collections.ObjectModel.Collection[$($objParams.TypeName)]
104+
$suggestedCorrections.add($correctionExtent) | Out-Null
105+
106+
[Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord]@{
107+
"Message" = "This is a rule with a suggested correction"
108+
"Extent" = $ast.Extent
109+
"RuleName" = $PSCmdlet.MyInvocation.InvocationName
110+
"Severity" = "Warning"
111+
"Severity" = "Warning"
112+
"RuleSuppressionID" = "MyRuleSuppressionID"
113+
"SuggestedCorrections" = $suggestedCorrections
114+
}
115+
```
116+
117+
### Make sure you export the function(s)
118+
119+
```powershell
120+
Export-ModuleMember -Function (FunctionName)
121+
```
122+
123+
## Example rule function
124+
125+
```powershell
126+
<#
127+
.SYNOPSIS
128+
Uses #Requires -RunAsAdministrator instead of your own methods.
129+
.DESCRIPTION
130+
The #Requires statement prevents a script from running unless the Windows PowerShell
131+
version, modules, snap-ins, and module and snap-in version prerequisites are met.
132+
From Windows PowerShell 4.0, the #Requires statement let script developers require that
133+
sessions be run with elevated user rights (run as Administrator). Script developers does
134+
not need to write their own methods any more. To fix a violation of this rule, please
135+
consider using #Requires -RunAsAdministrator instead of your own methods.
136+
.EXAMPLE
137+
Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst
138+
.INPUTS
139+
[System.Management.Automation.Language.ScriptBlockAst]
140+
.OUTPUTS
141+
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
142+
.NOTES
143+
None
144+
#>
145+
function Measure-RequiresRunAsAdministrator
146+
{
147+
[CmdletBinding()]
148+
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
149+
Param
150+
(
151+
[Parameter(Mandatory = $true)]
152+
[ValidateNotNullOrEmpty()]
153+
[System.Management.Automation.Language.ScriptBlockAst]
154+
$ScriptBlockAst
155+
)
156+
157+
Process
158+
{
159+
$results = @()
160+
try
161+
{
162+
#region Define predicates to find ASTs.
163+
# Finds specific method, IsInRole.
164+
[ScriptBlock]$predicate1 = {
165+
param ([System.Management.Automation.Language.Ast]$Ast)
166+
[bool]$returnValue = $false
167+
if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst])
168+
{
169+
[System.Management.Automation.Language.MemberExpressionAst]$meAst = $Ast
170+
if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst])
171+
{
172+
[System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member
173+
if ($sceAst.Value -eq 'isinrole')
174+
{
175+
$returnValue = $true
176+
}
177+
}
178+
}
179+
return $returnValue
180+
}
181+
182+
# Finds specific value, [system.security.principal.windowsbuiltinrole]::administrator.
183+
[ScriptBlock]$predicate2 = {
184+
param ([System.Management.Automation.Language.Ast]$Ast)
185+
[bool]$returnValue = $false
186+
if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst])
187+
{
188+
[System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast
189+
if ($asAst.Right.ToString() -eq '[system.security.principal.windowsbuiltinrole]::administrator')
190+
{
191+
$returnValue = $true
192+
}
193+
}
194+
return $returnValue
195+
}
196+
#endregion
197+
#region Finds ASTs that match the predicates.
198+
199+
[System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true)
200+
[System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true)
201+
if ($null -ne $ScriptBlockAst.ScriptRequirements)
202+
{
203+
if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and
204+
($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0))
205+
{
206+
$result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{
207+
'Message' = $Messages.MeasureRequiresRunAsAdministrator
208+
'Extent' = $assignmentAst.Extent
209+
'RuleName' = $PSCmdlet.MyInvocation.InvocationName
210+
'Severity' = 'Information'
211+
}
212+
$results += $result
213+
}
214+
}
215+
else
216+
{
217+
if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0))
218+
{
219+
$result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{
220+
'Message' = $Messages.MeasureRequiresRunAsAdministrator
221+
'Extent' = $assignmentAst.Extent
222+
'RuleName' = $PSCmdlet.MyInvocation.InvocationName
223+
'Severity' = 'Information'
224+
}
225+
$results += $result
226+
}
227+
}
228+
return $results
229+
#endregion
230+
}
231+
catch
232+
{
233+
$PSCmdlet.ThrowTerminatingError($PSItem)
234+
}
235+
}
236+
}
237+
```
238+
239+
More examples can be found in the
240+
[CommunityAnalyzerRules](https://github.com/PowerShell/PSScriptAnalyzer/tree/master/Tests/Engine/CommunityAnalyzerRules)
241+
folder on GitHub.

0 commit comments

Comments
 (0)