From 28abf77d7a9994bf4cb6a77cb51e909c0494e36c Mon Sep 17 00:00:00 2001 From: Stefan Vacareanu Date: Wed, 29 Jul 2026 19:43:02 +0200 Subject: [PATCH] Bump PSScriptAnalyzer to 1.25.0 Regenerates docs/patterns.json and docs/description/* via scripts/generateDocs.sh against the new module version, adding 5 new rules (disabled by default, matching current policy for non-security rules) and refreshing upstream rule descriptions/docs. Also truncates rule descriptions over 500 chars in install.ps1: upstream's Get-ScriptAnalyzerRule now returns longer text for AvoidUsingCmdletAliases and AvoidUsingInvokeExpression, which broke codacy-plugins-test's description length check. Co-Authored-By: Claude Sonnet 5 --- docs/description/description.json | 47 ++- .../description/psalignassignmentstatement.md | 160 +++++++- docs/description/psavoidlonglines.md | 15 +- .../psavoidreservedwordsasfunctionnames.md | 44 ++ .../psuseconsistentparametersetname.md | 135 +++++++ .../psuseconsistentparameterskind.md | 57 +++ .../psuseconstrainedlanguagemode.md | 378 ++++++++++++++++++ docs/description/psusecorrectcasing.md | 43 +- .../psusesinglevaluefrompipelineparameter.md | 101 +++++ docs/patterns.json | 32 +- install.ps1 | 1 + psscriptanalyzer.version | 2 +- 12 files changed, 965 insertions(+), 50 deletions(-) create mode 100644 docs/description/psavoidreservedwordsasfunctionnames.md create mode 100644 docs/description/psuseconsistentparametersetname.md create mode 100644 docs/description/psuseconsistentparameterskind.md create mode 100644 docs/description/psuseconstrainedlanguagemode.md create mode 100644 docs/description/psusesinglevaluefrompipelineparameter.md diff --git a/docs/description/description.json b/docs/description/description.json index c366d51..85a1bd8 100644 --- a/docs/description/description.json +++ b/docs/description/description.json @@ -7,12 +7,12 @@ { "patternId": "psavoidusingcmdletaliases", "title": "AvoidUsingCmdletAliases", - "description": "An alias is an alternate name for a cmdlet or command element like a function or script. An implicit alias omits the 'Get-' prefix for commands. However, when writing scripts for long-term maintenance, consider using the full cmdlet name instead of aliases. Aliases can reduce readability, understandability, and availability, making scripts harder to maintain by others or even the original author over time." + "description": "An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandabi..." }, { "patternId": "psavoidassignmenttoautomaticvariable", "title": "Checks for assignment to automatic variables", - "description": "This automatic variables is built into PowerShell and readonly." + "description": "This automatic variable is built into PowerShell and readonly." }, { "patternId": "psavoiddefaultvalueswitchparameter", @@ -27,7 +27,7 @@ { "patternId": "psavoidusingemptycatchblock", "title": "AvoidUsingEmptyCatchBlock", - "description": "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." + "description": "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 cause problems, it can, so it should be avoided where possible. To fix a violation of this rule, use Write-Error or throw statements in catch blocks." }, { "patternId": "psavoidexclaimoperator", @@ -62,7 +62,7 @@ { "patternId": "psavoidmultipletypeattributes", "title": "Check that parameter does not be assigned to multiple types", - "description": "Prameter should not have more than one type specifier." + "description": "Parameter should not have more than one type specifier." }, { "patternId": "psavoidnulloremptyhelpmessageattribute", @@ -77,7 +77,7 @@ { "patternId": "psavoidusingpositionalparameters", "title": "AvoidUsingPositionalParameters", - "description": "Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command." + "description": "Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using named parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command." }, { "patternId": "psreservedcmdletchar", @@ -89,6 +89,11 @@ "title": "ReservedParams", "description": "Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs." }, + { + "patternId": "psavoidreservedwordsasfunctionnames", + "title": "AvoidReservedWordsAsFunctionNames", + "description": "Avoid using reserved words as function names. Using reserved words as function names can cause errors or unexpected behavior in scripts." + }, { "patternId": "psavoidsemicolonsaslineterminators", "title": "Checks for lines that end with a semicolon", @@ -142,7 +147,7 @@ { "patternId": "psavoidusinginvokeexpression", "title": "Check to make sure that Invoke-Expression is not used", - "description": "The Invoke-Expression cmdlet runs a specified string as a command and returns its results. While powerful, it should be used cautiously, especially when handling user-provided data, as it can lead to code injection vulnerabilities. If the data comes from the program itself, it may be safe, but when user data is involved, proper precautions are needed. To fix a violation, remove Invoke-Expression and explore alternative solutions." + "description": "The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. It can be extraordinarily powerful so it is not that you want to never use it but you need to be very careful about using it. In particular, you are probably on safe ground if the data only comes from the program itself. If you include any data provided from the user - you need to protect yourself from Code Injection. To fix a violation of this rule, please re..." }, { "patternId": "psavoidusingplaintextforpassword", @@ -177,7 +182,7 @@ { "patternId": "psmisleadingbacktick", "title": "Checks that lines don't end with a backtick followed by whitespace", - "description": "Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace." + "description": "Ending a line with an escaped whitespace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace." }, { "patternId": "psmissingmodulemanifestfield", @@ -197,7 +202,7 @@ { "patternId": "pspossibleincorrectcomparisonwithnull", "title": "Analyzes the ast to check that $null is on the left side of any equality comparisons", - "description": "Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case." + "description": "Checks that $null is on the left side of any equality comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comparison are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case." }, { "patternId": "pspossibleincorrectusageofassignmentoperator", @@ -242,12 +247,27 @@ { "patternId": "psuseconsistentindentation", "title": "Checks if indentation is consistent throughout the source file", - "description": "Each statement block should have a consistent indenation." + "description": "Each statement block should have a consistent indentation." + }, + { + "patternId": "psuseconsistentparametersetname", + "title": "Check for case-sensitive parameter set", + "description": "Parameter set names are case-sensitive in PowerShell. This rule checks for case mismatches between DefaultParameterSetName and ParameterSetName values, case mismatches between different ParameterSetName values, and missing DefaultParameterSetName when parameter sets are used." + }, + { + "patternId": "psuseconsistentparameterskind", + "title": "Checks if function parameters definition kind is same as preferred", + "description": "Use consistent parameters definition kind to prevent potential unexpected behavior with inline functions parameters or param() block." }, { "patternId": "psuseconsistentwhitespace", "title": "Checks if whitespace usage is consistent throughout the source file", - "description": "Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';')" + "description": "Check for whitespace between keyword and open paren/curly, around assignment operator ('='), around arithmetic operators and after separators (',' and ';')" + }, + { + "patternId": "psuseconstrainedlanguagemode", + "title": "Checks for patterns that indicate Constrained Language Mode should be considered", + "description": "Identifies script patterns that are restricted in Constrained Language Mode. Constrained Language Mode limits the types, cmdlets, and .NET methods that can be used to help secure PowerShell in environments requiring additional restrictions." }, { "patternId": "psusecorrectcasing", @@ -284,6 +304,11 @@ "title": "Analyzes the ast to check if ShouldProcess is included in Advanced functions if the Verb of the function could change system state", "description": "Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'." }, + { + "patternId": "psusesinglevaluefrompipelineparameter", + "title": "UseSingleValueFromPipelineParameter", + "description": "Use at most a single ValueFromPipeline parameter per parameter set to avoid undefined or unexpected behaviour." + }, { "patternId": "psusesingularnouns", "title": "Analyzes scripts to check that all defined cmdlets use singular nouns", @@ -292,7 +317,7 @@ { "patternId": "psusesupportsshouldprocess", "title": "Checks if a function defines Confirm and/or WhatIf parameters manually instead of using SupportsShouldProcess attribute", - "description": "Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess." + "description": "Commands typically provide Confirm and WhatIf parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a command needs Confirm and WhatIf parameters, then it should support ShouldProcess." }, { "patternId": "psusetoexportfieldsinmanifest", diff --git a/docs/description/psalignassignmentstatement.md b/docs/description/psalignassignmentstatement.md index c2709ac..28ca9f4 100644 --- a/docs/description/psalignassignmentstatement.md +++ b/docs/description/psalignassignmentstatement.md @@ -10,38 +10,55 @@ title: AlignAssignmentStatement ## Description -Consecutive assignment statements are more readable if they are aligned. By aligned, we imply that -the `equal` sign for all the assignment statements should be in the same column. +Consecutive assignment statements are more readable when they're aligned. +Assignments are considered aligned when their `equals` signs line up vertically. -The rule looks for key (property) value pairs in a hashtable (DSC configuration) to check if they -are aligned or not. Consider the following example in which the key value pairs are not aligned. +This rule looks at the key-value pairs in hashtables (including DSC +configurations) as well as enum definitions. + +Consider the following example which has a hashtable and enum which are not +aligned. ```powershell $hashtable = @{ - property1 = 'value' + property = 'value' anotherProperty = 'another value' } + +enum Enum { + member = 1 + anotherMember = 2 +} ``` Alignment in this case would look like the following. ```powershell $hashtable = @{ - property1 = 'value' + property = 'value' anotherProperty = 'another value' } + +enum Enum { + member = 1 + anotherMember = 2 +} ``` -The rule ignores hashtables in which the assignment statements are on the same line. For example, -the rule ignores `$h = {a = 1; b = 2}`. +The rule ignores any assignments within hashtables and enums which are on the +same line as others. For example, the rule ignores `$h = @{a = 1; b = 2}`. ## Configuration ```powershell Rules = @{ PSAlignAssignmentStatement = @{ - Enable = $true - CheckHashtable = $true + Enable = $true + CheckHashtable = $true + AlignHashtableKvpWithInterveningComment = $true + CheckEnum = $true + AlignEnumMemberWithInterveningComment = $true + IncludeValuelessEnumMembers = $true } } ``` @@ -52,8 +69,123 @@ Rules = @{ Enable or disable the rule during ScriptAnalyzer invocation. -#### CheckHashtable: bool (Default value is `$false`) +#### CheckHashtable: bool (Default value is `$true`) + +Enforce alignment of assignment statements in a hashtable and in a DSC +Configuration. There is only one setting for hashtable and DSC configuration +because the property value pairs in a DSC configuration are parsed as key-value +pairs of a hashtable. + +#### AlignHashtableKvpWithInterveningComment: bool (Default value is `$true`) + +Include key-value pairs in the alignment that have an intervening comment - that +is to say a comment between the key name and the equals sign. + +Consider the following: + +```powershell +$hashtable = @{ + property = 'value' + anotherProperty <#A Comment#> = 'another value' + anotherDifferentProperty = 'yet another value' +} +``` + +With this setting disabled, the line with the comment is ignored, and it would +be aligned like so: + +```powershell +$hashtable = @{ + property = 'value' + anotherProperty <#A Comment#> = 'another value' + anotherDifferentProperty = 'yet another value' +} +``` + +With it enabled, the comment line is included in alignment: + +```powershell +$hashtable = @{ + property = 'value' + anotherProperty <#A Comment#> = 'another value' + anotherDifferentProperty = 'yet another value' +} +``` + +#### CheckEnum: bool (Default value is `$true`) + +Enforce alignment of assignment statements of an Enum definition. + +#### AlignEnumMemberWithInterveningComment: bool (Default value is `$true`) + +Include enum members in the alignment that have an intervening comment - that +is to say a comment between the member name and the equals sign. -Enforce alignment of assignment statements in a hashtable and in a DSC Configuration. There is only -one switch for hasthable and DSC configuration because the property value pairs in a DSC -configuration are parsed as key-value pairs of a hashtable. +Consider the following: + +```powershell +enum Enum { + member = 1 + anotherMember <#A Comment#> = 2 + anotherDifferentMember = 3 +} +``` + +With this setting disabled, the line with the comment is ignored, and it would +be aligned like so: + +```powershell +enum Enum { + member = 1 + anotherMember <#A Comment#> = 2 + anotherDifferentMember = 3 +} +``` + +With it enabled, the comment line is included in alignment: + +```powershell +enum Enum { + member = 1 + anotherMember <#A Comment#> = 2 + anotherDifferentMember = 3 +} +``` + +#### IncludeValuelessEnumMembers: bool (Default value is `$true`) + +Include enum members in the alignment that don't have an initial value - that +is to say they don't have an equals sign. Enum's don't need to be given a value +when they're defined. + +Consider the following: + +```powershell +enum Enum { + member = 1 + anotherMember = 2 + anotherDifferentMember +} +``` + +With this setting disabled the third line which has no value is not considered +when choosing where to align assignments. It would be aligned like so: + +```powershell +enum Enum { + member = 1 + anotherMember = 2 + anotherDifferentMember +} +``` + +With it enabled, the valueless member is included in alignment as if it had a +value: + +```powershell +enum Enum { + member = 1 + anotherMember = 2 + anotherDifferentMember +} +``` \ No newline at end of file diff --git a/docs/description/psavoidlonglines.md b/docs/description/psavoidlonglines.md index cc2603c..c36daaa 100644 --- a/docs/description/psavoidlonglines.md +++ b/docs/description/psavoidlonglines.md @@ -1,6 +1,6 @@ --- description: Avoid long lines -ms.date: 06/28/2023 +ms.date: 04/29/2025 ms.topic: reference title: AvoidLongLines --- @@ -10,10 +10,11 @@ title: AvoidLongLines ## Description -Lines should be no longer than a configured number of characters (default: 120), including leading -whitespace (indentation). +The length of lines, including leading spaces (indentation), should be less than the configured number +of characters. The default length is 120 characters. -**Note**: This rule is not enabled by default. The user needs to enable it through settings. +> [!NOTE] +> This rule isn't enabled by default. The user needs to enable it through settings. ## Configuration @@ -26,12 +27,12 @@ Rules = @{ } ``` -### Parameters +## Parameters -#### Enable: bool (Default value is `$false`) +### `Enable`: bool (Default value is `$false`) Enable or disable the rule during ScriptAnalyzer invocation. -#### MaximumLineLength: int (Default value is 120) +### `MaximumLineLength`: int (Default value is 120) Optional parameter to override the default maximum line length. diff --git a/docs/description/psavoidreservedwordsasfunctionnames.md b/docs/description/psavoidreservedwordsasfunctionnames.md new file mode 100644 index 0000000..769cbb8 --- /dev/null +++ b/docs/description/psavoidreservedwordsasfunctionnames.md @@ -0,0 +1,44 @@ +--- +description: Avoid reserved words as function names +ms.date: 08/31/2025 +ms.topic: reference +title: AvoidReservedWordsAsFunctionNames +--- +# AvoidReservedWordsAsFunctionNames + +**Severity Level: Warning** + +## Description + +Avoid using reserved words as function names. Using reserved words as function names can cause +errors or unexpected behavior in scripts. + +## How to Fix + +Avoid using any of the reserved words as function names. Choose a different name that's not a +reserved word. + +See [about_Reserved_Words][01] for a list of reserved words in PowerShell. + +## Example + +### Wrong + +```powershell +# Function is a reserved word +function function { + Write-Host "Hello, World!" +} +``` + +### Correct + +```powershell +# myFunction is not a reserved word +function myFunction { + Write-Host "Hello, World!" +} +``` + + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_reserved_words diff --git a/docs/description/psuseconsistentparametersetname.md b/docs/description/psuseconsistentparametersetname.md new file mode 100644 index 0000000..5ad33eb --- /dev/null +++ b/docs/description/psuseconsistentparametersetname.md @@ -0,0 +1,135 @@ +--- +description: Use consistent parameter set names and proper parameter set configuration. +ms.date: 08/19/2025 +ms.topic: reference +title: UseConsistentParameterSetName +--- + +# UseConsistentParameterSetName + +**Severity Level: Warning** + +## Description + +Parameter set names in PowerShell are case-sensitive, unlike most other PowerShell elements. This rule ensures consistent casing and proper configuration of parameter sets to avoid runtime errors and improve code clarity. + +The rule performs five different checks: + +1. **Missing DefaultParameterSetName** - Warns when parameter sets are used but no default is specified +2. **Multiple parameter declarations** - Detects when a parameter is declared multiple times in the same parameter set. This is ultimately a runtime exception - this check helps catch it sooner. +3. **Case mismatch between DefaultParameterSetName and ParameterSetName** - Ensures consistent casing +4. **Case mismatch between different ParameterSetName values** - Ensures all references to the same parameter set use identical casing +5. **Parameter set names containing newlines** - Warns against using newline characters in parameter set names + +> [!NOTE] +> This rule is not enabled by default. The user needs to enable it through settings. + +## How + +- Use a `DefaultParameterSetName` when defining multiple parameter sets +- Ensure consistent casing between `DefaultParameterSetName` and `ParameterSetName` values +- Use identical casing for all references to the same parameter set name +- Avoid declaring the same parameter multiple times in a single parameter set +- Do not use newline characters in parameter set names + +## Example + +### Wrong + +```powershell +# Missing DefaultParameterSetName +function Get-Data { + [CmdletBinding()] + param( + [Parameter(ParameterSetName='ByName')] + [string]$Name, + + [Parameter(ParameterSetName='ByID')] + [int]$ID + ) +} + +# Case mismatch between DefaultParameterSetName and ParameterSetName +function Get-Data { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='byname')] + [string]$Name, + + [Parameter(ParameterSetName='ByID')] + [int]$ID + ) +} + +# Inconsistent casing between ParameterSetName values +function Get-Data { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName')] + [string]$Name, + + [Parameter(ParameterSetName='byname')] + [string]$DisplayName + ) +} + +# Multiple parameter declarations in same set +function Get-Data { + param( + [Parameter(ParameterSetName='ByName')] + [Parameter(ParameterSetName='ByName')] + [string]$Name + ) +} + +# Parameter set name with newline +function Get-Data { + param( + [Parameter(ParameterSetName="Set`nOne")] + [string]$Name + ) +} +``` + +### Correct + +```powershell +# Proper parameter set configuration +function Get-Data { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName', Mandatory)] + [string]$Name, + + [Parameter(ParameterSetName='ByName')] + [Parameter(ParameterSetName='ByID')] + [string]$ComputerName, + + [Parameter(ParameterSetName='ByID', Mandatory)] + [int]$ID + ) +} +``` + +## Configuration + +```powershell +Rules = @{ + PSUseConsistentParameterSetName = @{ + Enable = $true + } +} +``` + +### Parameters + +- `Enable`: **bool** (Default value is `$false`) + + Enable or disable the rule during ScriptAnalyzer invocation. + +## Notes + +- Parameter set names are case-sensitive in PowerShell, making this different from most other PowerShell elements +- The first occurrence of a parameter set name in your code is treated as the canonical casing +- Parameters without [Parameter()] attributes are automatically part of all parameter sets +- It's a PowerShell best practice to always specify a DefaultParameterSetName when using parameter sets \ No newline at end of file diff --git a/docs/description/psuseconsistentparameterskind.md b/docs/description/psuseconsistentparameterskind.md new file mode 100644 index 0000000..04a323b --- /dev/null +++ b/docs/description/psuseconsistentparameterskind.md @@ -0,0 +1,57 @@ +# UseConsistentParametersKind + +**Severity Level: Warning** + +## Description + +All functions should have same parameters definition kind specified in the rule. +Possible kinds are: +1. `Inline`, i.e.: +```PowerShell +function f([Parameter()]$FirstParam) { + return +} +``` +2. `ParamBlock`, i.e.: +```PowerShell +function f { + param([Parameter()]$FirstParam) + return +} +``` + +* For information: in simple scenarios both function definitions above may be considered as equal. Using this rule as-is is more for consistent code-style than functional, but it can be useful in combination with other rules. + +## How to Fix + +Rewrite function so it defines parameters as specified in the rule + +## Example + +### When the rule sets parameters definition kind to 'Inline': +```PowerShell +# Correct +function f([Parameter()]$FirstParam) { + return +} + +# Incorrect +function g { + param([Parameter()]$FirstParam) + return +} +``` + +### When the rule sets parameters definition kind to 'ParamBlock': +```PowerShell +# Inorrect +function f([Parameter()]$FirstParam) { + return +} + +# Correct +function g { + param([Parameter()]$FirstParam) + return +} +``` \ No newline at end of file diff --git a/docs/description/psuseconstrainedlanguagemode.md b/docs/description/psuseconstrainedlanguagemode.md new file mode 100644 index 0000000..50ccfd0 --- /dev/null +++ b/docs/description/psuseconstrainedlanguagemode.md @@ -0,0 +1,378 @@ +--- +description: Use patterns compatible with Constrained Language Mode +ms.date: 03/17/2026 +ms.topic: reference +title: UseConstrainedLanguageMode +--- +# UseConstrainedLanguageMode + +**Severity Level: Warning** + +## Description + +This rule identifies PowerShell patterns that are restricted or not permitted in Constrained Language Mode (CLM). + +Constrained Language Mode is a PowerShell security feature that restricts: +- .NET types that can be used +- COM objects that can be instantiated +- Commands that can be executed +- Language features that can be used + +CLM is commonly used in: +- Application Control environments (Application Control for Business, AppLocker) +- Just Enough Administration (JEA) endpoints +- Secure environments requiring additional PowerShell restrictions + +**Signed Script Behavior**: Digitally signed scripts from trusted publishers execute in Full Language Mode (FLM) even in CLM environments. The rule detects signature blocks (`# SIG # Begin signature block`) and adjusts checks accordingly - most restrictions don't apply to signed scripts, but certain checks (dot-sourcing, parameter types, manifest best practices) are always enforced. + +**Important**: The rule performs a simple text check for signature blocks and does NOT validate signature authenticity or certificate trust. Actual signature validation is performed by PowerShell at runtime. + +## Constrained Language Mode Restrictions + +### Unsigned Scripts (Full CLM Checking) + +The following are flagged for unsigned scripts: + +1. **Add-Type** - Code compilation not permitted +2. **Disallowed COM Objects** - Only Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp allowed +3. **Disallowed .NET Types** - Only ~70 allowed types (string, int, hashtable, pscredential, etc.) +4. **Type Constraints** - On parameters and variables +5. **Type Expressions** - Static type references like `[Type]::Method()` +6. **Type Casts** - Converting to disallowed types +7. **Member Invocations** - Methods/properties on disallowed types +8. **PowerShell Classes** - `class` keyword not permitted +9. **XAML/WPF** - Not permitted +10. **Invoke-Expression** - Restricted +11. **Dot-Sourcing** - May be restricted depending on the file being sourced +12. **Module Manifest Wildcards** - Wildcard exports not recommended +13. **Module Manifest .ps1 Files** - Script modules ending with .ps1 not allowed + +Always enforced, even for signed scripts + +### Signed Scripts (Selective Checking) + +For scripts with signature blocks, only these are checked: +- Dot-sourcing +- Parameter type constraints +- Module manifest wildcards (.psd1 files) +- Module manifest script modules (.psd1 files) + +## Configuration + +### Basic Configuration + +```powershell +@{ + Rules = @{ + PSUseConstrainedLanguageMode = @{ + Enable = $true + } + } +} +``` + +### Parameters + +#### Enable: bool (Default value is `$false`) + +Enable or disable the rule during ScriptAnalyzer invocation. This rule is disabled by default because not all scripts need CLM compatibility. + +#### IgnoreSignatures: bool (Default value is `$false`) + +Control signature detection behavior: + +- `$false` (default): Automatically detect signatures. Signed scripts get selective checking, unsigned get full checking. +- `$true`: Bypass signature detection. ALL scripts get full CLM checking regardless of signature status. + +```powershell +@{ + Rules = @{ + PSUseConstrainedLanguageMode = @{ + Enable = $true + IgnoreSignatures = $true # Enforce full CLM compliance for all scripts + } + } +} +``` + +**Use `IgnoreSignatures = $true` when:** +- Auditing signed scripts for complete CLM compatibility +- Preparing scripts for untrusted environments +- Enforcing strict CLM compliance organization-wide +- Development/testing to see all potential issues + +## How to Fix + +### Replace Add-Type + +Use allowed cmdlets or pre-compile assemblies. + +### Replace Disallowed COM Objects + +Use only allowed COM objects (Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp) or PowerShell cmdlets. + +### Replace Disallowed Types + +Use allowed type accelerators (`[string]`, `[int]`, `[hashtable]`, etc.) or allowed cmdlets instead of disallowed .NET types. + +### Replace PowerShell Classes + +Use `New-Object PSObject` with `Add-Member` or hashtables instead of classes. + +**Important**: `[PSCustomObject]@{}` syntax is NOT allowed in CLM because it uses type casting. + +### Avoid XAML + +Don't use WPF/XAML in CLM-compatible scripts. + +### Replace Invoke-Expression + +Use direct execution (`&`) or safer alternatives. + +### Replace Dot-Sourcing + +Use modules with Import-Module instead of dot-sourcing when possible. + +### Fix Module Manifests + +- Replace wildcard exports (`*`) with explicit lists +- Use `.psm1` or `.dll` instead of `.ps1` for RootModule/NestedModules +- Don't use ScriptsToProcess as it loads in the caller's scope and will be blocked. + +## Examples + +### Example 1: Add-Type + +#### Wrong + +```powershell +Add-Type -TypeDefinition @" + public class Helper { + public static string DoWork() { return "Done"; } + } +"@ +``` + +#### Correct + +```powershell + # Code sign your scripts/modules using proper signing tools + # (for example, Set-AuthenticodeSignature or external signing processes) + # Use allowed cmdlets instead of Add-Type-defined types where possible + # Or pre-compile, sign, and load the assembly (for example, via Add-Type -Path) +``` + +### Example 2: COM Objects + +#### Wrong + +```powershell +$excel = New-Object -ComObject Excel.Application +``` + +#### Correct + +```powershell +# Use allowed COM object +$dict = New-Object -ComObject Scripting.Dictionary + +# Or use PowerShell cmdlets +Import-Excel -Path $file # From ImportExcel module +``` + +### Example 3: Disallowed Types + +#### Wrong + +```powershell +# Type constraint and member invocation flagged +function Download-File { + param([System.Net.WebClient]$Client) + $Client.DownloadString($url) +} + +# Type cast and method call flagged +[System.Net.WebClient]$client = New-Object System.Net.WebClient +$data = $client.DownloadData($url) +``` + +#### Correct + +```powershell +# Use allowed cmdlets +function Download-File { + param([string]$Url) + Invoke-WebRequest -Uri $Url +} + +# Use allowed types +function Process-Text { + param([string]$Text) + $upper = $Text.ToUpper() # String methods are allowed +} +``` + +### Example 4: PowerShell Classes + +#### Wrong + +```powershell +class MyClass { + [string]$Name + + [string]GetInfo() { + return $this.Name + } +} + +# Also wrong - uses type cast +$obj = [PSCustomObject]@{ + Name = "Test" +} +``` + +#### Correct + +```powershell +# Option 1: New-Object PSObject with Add-Member +$obj = New-Object PSObject -Property @{ + Name = "Test" +} + +$obj | Add-Member -MemberType ScriptMethod -Name GetInfo -Value { + return $this.Name +} + +Add-Member -InputObject $obj -NotePropertyMembers @{"Number" = 42} + +# Option 2: Hashtable +$obj = @{ + Name = "Test" + Number = 42 +} +``` + +### Example 5: Module Manifests + +#### Wrong + +```powershell +@{ + ModuleVersion = '1.0.0' + RootModule = 'MyModule.ps1' # .ps1 not recommended + FunctionsToExport = '*' # Wildcard not recommended + CmdletsToExport = '*' +} +``` + +#### Correct + +```powershell +@{ + ModuleVersion = '1.0.0' + RootModule = 'MyModule.psm1' # Use .psm1 or .dll + FunctionsToExport = @( # Explicit list + 'Get-MyFunction' + 'Set-MyFunction' + ) + CmdletsToExport = @() +} +``` + +### Example 6: Array Types + +#### Wrong + +```powershell +# Disallowed type in array +param([System.Net.WebClient[]]$Clients) +``` + +#### Correct + +```powershell +# Allowed types in arrays are fine +param([string[]]$Names) +param([int[]]$Numbers) +param([hashtable[]]$Configuration) +``` + +## Detailed Restrictions + +### 1. Add-Type +`Add-Type` allows compiling arbitrary C# code and is not permitted in CLM. + +**Enforced For**: Unsigned scripts only + +### 2. COM Objects +Only three COM objects are allowed: +- `Scripting.Dictionary` +- `Scripting.FileSystemObject` +- `VBScript.RegExp` + +All others (Excel.Application, WScript.Shell, etc.) are flagged. + +**Enforced For**: Unsigned scripts only + +### 3. .NET Types +Only ~70 allowed types including: +- Primitives: `string`, `int`, `bool`, `byte`, `char`, `datetime`, `decimal`, `double`, etc. +- Collections: `hashtable`, `array`, `arraylist` +- PowerShell: `pscredential`, `psobject`, `securestring` +- Utilities: `regex`, `guid`, `version`, `uri`, `xml` +- Arrays: `string[]`, `int[][]`, etc. (array of any allowed type) + +The rule checks type usage in: +- Parameter type constraints (**always enforced, even for signed scripts**) +- Variable type constraints +- New-Object -TypeName +- Type expressions (`[Type]::Method()`) +- Type casts (`[Type]$variable`) +- Member invocations on typed variables + +**Enforced For**: Parameter constraints always; others unsigned only + +### 4. PowerShell Classes +The `class` keyword is not permitted. Use `New-Object PSObject` with `Add-Member` or hashtables. + +**Note**: `[PSCustomObject]@{}` is also not allowed because it uses type casting. + +**Enforced For**: Unsigned scripts only + +### 5. XAML/WPF +XAML and WPF are not permitted in CLM. + +**Enforced For**: Unsigned scripts only + +### 6. Invoke-Expression +`Invoke-Expression` is restricted in CLM. + +**Enforced For**: Unsigned scripts only + +### 7. Dot-Sourcing +Dot-sourcing (`. $PSScriptRoot\script.ps1`) may be restricted depending on source location. + +**Enforced For**: ALL scripts (unsigned and signed) + +### 8. Module Manifest Best Practices + +#### Wildcard Exports +Don't use `*` in: `FunctionsToExport`, `CmdletsToExport`, `AliasesToExport`, `VariablesToExport` + +Use explicit lists for security and clarity. + +**Enforced For**: ALL .psd1 files (unsigned and signed) + +#### Script Module Files +Don't use `.ps1` files in: `RootModule`, `ModuleToProcess`, `NestedModules` + +Use `.psm1` (script modules) or `.dll` (binary modules) for better performance and compatibility. + +**Enforced For**: ALL .psd1 files (unsigned and signed) + +## More Information + +- [About Language Modes](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes) +- [PowerShell Constrained Language Mode](https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode/) +- [PowerShell Module Function Export in Constrained Language](https://devblogs.microsoft.com/powershell/powershell-module-function-export-in-constrained-language/) +- [PowerShell Constrained Language Mode and the Dot-Source Operator](https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode-and-the-dot-source-operator/) diff --git a/docs/description/psusecorrectcasing.md b/docs/description/psusecorrectcasing.md index 6afcb3a..b73df64 100644 --- a/docs/description/psusecorrectcasing.md +++ b/docs/description/psusecorrectcasing.md @@ -1,6 +1,6 @@ --- description: Use exact casing of cmdlet/function/parameter name. -ms.date: 06/28/2023 +ms.date: 03/19/2025 ms.topic: reference title: UseCorrectCasing --- @@ -10,16 +10,22 @@ title: UseCorrectCasing ## Description -This is a style formatting rule. PowerShell is case insensitive where applicable. The casing of -cmdlet names or parameters does not matter but this rule ensures that the casing matches for -consistency and also because most cmdlets/parameters start with an upper case and using that -improves readability to the human eye. +This is a style/formatting rule. PowerShell is case insensitive wherever possible, so the casing of +cmdlet names, parameters, keywords and operators doesn't matter. This rule nonetheless ensures +consistent casing for clarity and readability. Using lowercase keywords helps distinguish them from +commands. Using lowercase operators helps distinguish them from parameters. + +## How + +- Use exact casing for type names. +- Use exact casing of the cmdlet and its parameters. +- Use lowercase for language keywords and operators. ## Configuration ```powershell Rules = @{ - PS UseCorrectCasing = @{ + PSUseCorrectCasing = @{ Enable = $true CheckCommands = $true CheckKeyword = $true @@ -28,13 +34,15 @@ Rules = @{ } ``` +## Parameters + ### Enable: bool (Default value is `$false`) Enable or disable the rule during ScriptAnalyzer invocation. ### CheckCommands: bool (Default value is `$true`) -If true, require the case of all operators to be lowercase. +If true, require the case of all command and parameter names to match their canonical casing. ### CheckKeyword: bool (Default value is `$true`) @@ -42,23 +50,26 @@ If true, require the case of all keywords to be lowercase. ### CheckOperator: bool (Default value is `$true`) -If true, require the case of all commands to match their actual casing. - -## How +If true, require the case of all operators to be lowercase. For example: `-eq`, `-ne`, `-gt` -Use exact casing of the cmdlet and its parameters, e.g. -`Invoke-Command { 'foo' } -RunAsAdministrator`. +## Examples -## Example - -### Wrong +### Wrong way ```powershell +ForEach ($file in Get-childitem -Recurse) { + $file.Extension -EQ '.txt' +} + invoke-command { 'foo' } -runasadministrator ``` -### Correct +### Correct way ```powershell +foreach ($file in Get-ChildItem -Recurse) { + $file.Extension -eq '.txt' +} + Invoke-Command { 'foo' } -RunAsAdministrator ``` diff --git a/docs/description/psusesinglevaluefrompipelineparameter.md b/docs/description/psusesinglevaluefrompipelineparameter.md new file mode 100644 index 0000000..bfaa3fe --- /dev/null +++ b/docs/description/psusesinglevaluefrompipelineparameter.md @@ -0,0 +1,101 @@ +--- +description: Use at most a single ValueFromPipeline parameter per parameter set. +ms.date: 08/08/2025 +ms.topic: reference +title: UseSingleValueFromPipelineParameter +--- +# UseSingleValueFromPipelineParameter + +**Severity Level: Warning** + +## Description + +Parameter sets should have at most one parameter marked as +`ValueFromPipeline = true`. + +This rule identifies functions where multiple parameters within the same +parameter set have `ValueFromPipeline` set to `true` (either explicitly or +implicitly). + +## How + +Ensure that only one parameter per parameter set accepts pipeline input by +value. If you need multiple parameters to accept different types of pipeline +input, use separate parameter sets. + +## Example + +### Wrong + +```powershell +function Process-Data { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [string] $InputData, + + [Parameter(ValueFromPipeline)] + [string] $ProcessingMode + ) + + process { + Write-Output "$ProcessingMode`: $InputData" + } +} +``` + + +### Correct + +```powershell +function Process-Data { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [string] $InputData, + + [Parameter(Mandatory)] + [string] $ProcessingMode + ) + process { + Write-Output "$ProcessingMode`: $InputData" + } +} +``` +## Suppression + +To suppress this rule for a specific parameter set, use the `SuppressMessage` +attribute with the parameter set name: + +```powershell +function Process-Data { + [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'MyParameterSet')] + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')] + [string] $InputData, + + [Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')] + [string] $ProcessingMode + ) + process { + Write-Output "$ProcessingMode`: $InputData" + } +} +``` + +For the default parameter set, use `'default'` as the suppression target: + +```powershell +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'default')] +``` + +## Notes + +- This rule applies to both explicit `ValueFromPipeline = $true` and implicit + `ValueFromPipeline` (which is the same as using `= $true`) +- Parameters with `ValueFromPipeline=$false` are not flagged by this rule +- The rule correctly handles the default parameter set (`__AllParameterSets`) + and named parameter sets +- Different parameter sets can each have their own single `ValueFromPipeline` + parameter without triggering this rule diff --git a/docs/patterns.json b/docs/patterns.json index 64f0060..6e4b54b 100644 --- a/docs/patterns.json +++ b/docs/patterns.json @@ -1,6 +1,6 @@ { "name": "psscriptanalyzer", - "version": "1.24.0", + "version": "1.25.0", "patterns": [ { "patternId": "psalignassignmentstatement", @@ -110,6 +110,12 @@ "category": "ErrorProne", "enabled": true }, + { + "patternId": "psavoidreservedwordsasfunctionnames", + "level": "Warning", + "category": "ErrorProne", + "enabled": false + }, { "patternId": "psavoidsemicolonsaslineterminators", "level": "Warning", @@ -301,12 +307,30 @@ "category": "ErrorProne", "enabled": false }, + { + "patternId": "psuseconsistentparametersetname", + "level": "Warning", + "category": "ErrorProne", + "enabled": false + }, + { + "patternId": "psuseconsistentparameterskind", + "level": "Warning", + "category": "ErrorProne", + "enabled": false + }, { "patternId": "psuseconsistentwhitespace", "level": "Warning", "category": "ErrorProne", "enabled": false }, + { + "patternId": "psuseconstrainedlanguagemode", + "level": "Warning", + "category": "ErrorProne", + "enabled": false + }, { "patternId": "psusecorrectcasing", "level": "Info", @@ -350,6 +374,12 @@ "category": "ErrorProne", "enabled": true }, + { + "patternId": "psusesinglevaluefrompipelineparameter", + "level": "Warning", + "category": "ErrorProne", + "enabled": false + }, { "patternId": "psusesingularnouns", "level": "Warning", diff --git a/install.ps1 b/install.ps1 index 9f816c0..f102f10 100644 --- a/install.ps1 +++ b/install.ps1 @@ -128,6 +128,7 @@ foreach($pat in $patterns) { $title = getTitle $patternNameCamelCased ; if($title -eq $patternNameCamelCased) { Write-Output "$patternNameCamelCased"; $count = $count+1;} $description = $pat.Description ; + if($description.Length -gt 500) { $description = $description.Substring(0, 497) + '...' } ; $level = if($pat.Severity -eq 'Information') { 'Info' } else { $pat.Severity.ToString() } ; $category = getCategory $level $patternId; $enabledByDefault = $enabledRules -contains $patternId diff --git a/psscriptanalyzer.version b/psscriptanalyzer.version index d437046..f2538f8 100644 --- a/psscriptanalyzer.version +++ b/psscriptanalyzer.version @@ -1 +1 @@ -1.24.0 \ No newline at end of file +1.25.0 \ No newline at end of file