diff --git a/.github/workflows/review-intelligencex.yml b/.github/workflows/review-intelligencex.yml index 31e9458c0..773d3ed09 100644 --- a/.github/workflows/review-intelligencex.yml +++ b/.github/workflows/review-intelligencex.yml @@ -76,6 +76,19 @@ jobs: fi echo "Changed files: $(wc -l < artifacts/changed-files.txt)" + - name: Tune reviewer budgets for large PRs + if: ${{ github.event_name == 'pull_request' }} + run: | + changed="$(wc -l < artifacts/changed-files.txt | tr -d ' ')" + catalog="$(grep -cE '^Analysis/Catalog/(rules|overrides)/' artifacts/changed-files.txt || true)" + if [ "${changed:-0}" -gt 30 ] || [ "${catalog:-0}" -gt 10 ]; then + echo "Detected large diff (changed=${changed}, catalog=${catalog}); expanding reviewer context limits." + echo "INPUT_MAX_FILES=200" >> "$GITHUB_ENV" + echo "INPUT_MAX_PATCH_CHARS=120000" >> "$GITHUB_ENV" + else + echo "Diff size within default limits (changed=${changed}, catalog=${catalog}); using defaults." + fi + - name: Static analysis gate run: dotnet run --project IntelligenceX.Cli/IntelligenceX.Cli.csproj --framework net8.0 -- analyze gate --config .intelligencex/reviewer.json --workspace . --changed-files artifacts/changed-files.txt diff --git a/Analysis/Catalog/overrides/powershell/PSAvoidDefaultValueForMandatoryParameter.json b/Analysis/Catalog/overrides/powershell/PSAvoidDefaultValueForMandatoryParameter.json new file mode 100644 index 000000000..ee63f37de --- /dev/null +++ b/Analysis/Catalog/overrides/powershell/PSAvoidDefaultValueForMandatoryParameter.json @@ -0,0 +1,4 @@ +{ + "id": "PSAvoidDefaultValueForMandatoryParameter", + "description": "Mandatory parameters should not be initialized with a default value in the param block because this value will be ignored. To fix a violation of this rule, avoid initializing a value for mandatory parameters in the param block." +} diff --git a/Analysis/Catalog/overrides/powershell/PSAvoidDefaultValueSwitchParameter.json b/Analysis/Catalog/overrides/powershell/PSAvoidDefaultValueSwitchParameter.json new file mode 100644 index 000000000..e84e8dee2 --- /dev/null +++ b/Analysis/Catalog/overrides/powershell/PSAvoidDefaultValueSwitchParameter.json @@ -0,0 +1,4 @@ +{ + "id": "PSAvoidDefaultValueSwitchParameter", + "description": "Switch parameters should not default to true." +} diff --git a/Analysis/Catalog/overrides/powershell/PSAvoidMultipleTypeAttributes.json b/Analysis/Catalog/overrides/powershell/PSAvoidMultipleTypeAttributes.json new file mode 100644 index 000000000..479139d1a --- /dev/null +++ b/Analysis/Catalog/overrides/powershell/PSAvoidMultipleTypeAttributes.json @@ -0,0 +1,4 @@ +{ + "id": "PSAvoidMultipleTypeAttributes", + "description": "Parameters should not have more than one type specifier." +} diff --git a/Analysis/Catalog/overrides/powershell/PSUseConsistentIndentation.json b/Analysis/Catalog/overrides/powershell/PSUseConsistentIndentation.json new file mode 100644 index 000000000..d3e40d564 --- /dev/null +++ b/Analysis/Catalog/overrides/powershell/PSUseConsistentIndentation.json @@ -0,0 +1,5 @@ +{ + "id": "PSUseConsistentIndentation", + "title": "Use Consistent Indentation", + "description": "Each statement block should have consistent indentation." +} diff --git a/Analysis/Catalog/rules/powershell/PSAlignAssignmentStatement.json b/Analysis/Catalog/rules/powershell/PSAlignAssignmentStatement.json new file mode 100644 index 000000000..4cd860165 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAlignAssignmentStatement.json @@ -0,0 +1,16 @@ +{ + "id": "PSAlignAssignmentStatement", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAlignAssignmentStatement", + "title": "Align Assignment Statements", + "description": "Line up assignment statements so that the assignment operators are aligned.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/alignassignmentstatement" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidAssignmentToAutomaticVariable.json b/Analysis/Catalog/rules/powershell/PSAvoidAssignmentToAutomaticVariable.json new file mode 100644 index 000000000..3623e7237 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidAssignmentToAutomaticVariable.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidAssignmentToAutomaticVariable", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidAssignmentToAutomaticVariable", + "title": "Changing automatic variables might have undesired side effects", + "description": "Automatic variables are built into PowerShell and are read-only. Avoid assigning to them.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidassignmenttoautomaticvariable" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidDefaultValueForMandatoryParameter.json b/Analysis/Catalog/rules/powershell/PSAvoidDefaultValueForMandatoryParameter.json new file mode 100644 index 000000000..1ab7961ac --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidDefaultValueForMandatoryParameter.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidDefaultValueForMandatoryParameter", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidDefaultValueForMandatoryParameter", + "title": "Avoid Default Value For Mandatory Parameter", + "description": "Mandatory parameter should not be initialized with a default value in the param block because this value will be ignored. To fix a violation of this rule, please avoid initializing a value for the mandatory parameter in the param block.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoiddefaultvalueformandatoryparameter" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidDefaultValueSwitchParameter.json b/Analysis/Catalog/rules/powershell/PSAvoidDefaultValueSwitchParameter.json new file mode 100644 index 000000000..9b71472d2 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidDefaultValueSwitchParameter.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidDefaultValueSwitchParameter", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidDefaultValueSwitchParameter", + "title": "Switch Parameters Should Not Default To True", + "description": "Switch parameter should not default to true.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoiddefaultvalueswitchparameter" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidExclaimOperator.json b/Analysis/Catalog/rules/powershell/PSAvoidExclaimOperator.json new file mode 100644 index 000000000..b372a9dd2 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidExclaimOperator.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidExclaimOperator", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidExclaimOperator", + "title": "Avoid exclaim operator", + "description": "The negation operator ! should not be used for readability purposes. Use -not instead.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidexclaimoperator" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidGlobalAliases.json b/Analysis/Catalog/rules/powershell/PSAvoidGlobalAliases.json new file mode 100644 index 000000000..6439ff0d2 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidGlobalAliases.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidGlobalAliases", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidGlobalAliases", + "title": "Avoid global aliases.", + "description": "Checks that global aliases are not used. Global aliases are strongly discouraged as they overwrite desired aliases with name conflicts.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidglobalaliases" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidGlobalFunctions.json b/Analysis/Catalog/rules/powershell/PSAvoidGlobalFunctions.json new file mode 100644 index 000000000..d3e461abb --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidGlobalFunctions.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidGlobalFunctions", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidGlobalFunctions", + "title": "Avoid global functions and aliases", + "description": "Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidglobalfunctions" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidGlobalVars.json b/Analysis/Catalog/rules/powershell/PSAvoidGlobalVars.json new file mode 100644 index 000000000..0dfb39f3b --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidGlobalVars.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidGlobalVars", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidGlobalVars", + "title": "No Global Variables", + "description": "Checks that global variables are not used. Global variables are strongly discouraged as they can cause errors across different systems.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidglobalvars" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidInvokingEmptyMembers.json b/Analysis/Catalog/rules/powershell/PSAvoidInvokingEmptyMembers.json new file mode 100644 index 000000000..3ae318fa1 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidInvokingEmptyMembers.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidInvokingEmptyMembers", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidInvokingEmptyMembers", + "title": "Avoid Invoking Empty Members", + "description": "Invoking non-constant members would cause potential bugs. Please double check the syntax to make sure members invoked are non-constant.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidinvokingemptymembers" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidLongLines.json b/Analysis/Catalog/rules/powershell/PSAvoidLongLines.json new file mode 100644 index 000000000..74cf1ebb9 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidLongLines.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidLongLines", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidLongLines", + "title": "Avoid long lines", + "description": "Line lengths should be less than the configured maximum.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidlonglines" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidMultipleTypeAttributes.json b/Analysis/Catalog/rules/powershell/PSAvoidMultipleTypeAttributes.json new file mode 100644 index 000000000..f0a20f784 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidMultipleTypeAttributes.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidMultipleTypeAttributes", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidMultipleTypeAttributes", + "title": "Avoid multiple type specifiers on parameters", + "description": "Parameter should not have more than one type specifier.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidmultipletypeattributes" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidNullOrEmptyHelpMessageAttribute.json b/Analysis/Catalog/rules/powershell/PSAvoidNullOrEmptyHelpMessageAttribute.json new file mode 100644 index 000000000..9e200b5dc --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidNullOrEmptyHelpMessageAttribute.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidNullOrEmptyHelpMessageAttribute", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidNullOrEmptyHelpMessageAttribute", + "title": "Avoid using null or empty HelpMessage parameter attribute.", + "description": "Setting the HelpMessage attribute to an empty string or null value causes PowerShell interpreter to throw an error while executing the corresponding function.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidnulloremptyhelpmessageattribute" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidOverwritingBuiltInCmdlets.json b/Analysis/Catalog/rules/powershell/PSAvoidOverwritingBuiltInCmdlets.json new file mode 100644 index 000000000..28ba7a9d5 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidOverwritingBuiltInCmdlets.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidOverwritingBuiltInCmdlets", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidOverwritingBuiltInCmdlets", + "title": "Avoid overwriting built in cmdlets", + "description": "Do not overwrite the definition of a cmdlet that is included with PowerShell.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidoverwritingbuiltincmdlets" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidSemicolonsAsLineTerminators.json b/Analysis/Catalog/rules/powershell/PSAvoidSemicolonsAsLineTerminators.json new file mode 100644 index 000000000..ef25a409b --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidSemicolonsAsLineTerminators.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidSemicolonsAsLineTerminators", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidSemicolonsAsLineTerminators", + "title": "Avoid semicolons as line terminators", + "description": "Line should not end with a semicolon.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidsemicolonsaslineterminators" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidShouldContinueWithoutForce.json b/Analysis/Catalog/rules/powershell/PSAvoidShouldContinueWithoutForce.json new file mode 100644 index 000000000..974c0d2bd --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidShouldContinueWithoutForce.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidShouldContinueWithoutForce", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidShouldContinueWithoutForce", + "title": "Avoid Using ShouldContinue Without Boolean Force Parameter", + "description": "Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidshouldcontinuewithoutforce" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidTrailingWhitespace.json b/Analysis/Catalog/rules/powershell/PSAvoidTrailingWhitespace.json new file mode 100644 index 000000000..210ca367e --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidTrailingWhitespace.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidTrailingWhitespace", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidTrailingWhitespace", + "title": "Avoid trailing whitespace", + "description": "Each line should have no trailing whitespace.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidtrailingwhitespace" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingAllowUnencryptedAuthentication.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingAllowUnencryptedAuthentication.json new file mode 100644 index 000000000..4a14a5723 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingAllowUnencryptedAuthentication.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingAllowUnencryptedAuthentication", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingAllowUnencryptedAuthentication", + "title": "Avoid AllowUnencryptedAuthentication Switch", + "description": "Avoid sending credentials and secrets over unencrypted connections.", + "category": "Security", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingallowunencryptedauthentication" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingBrokenHashAlgorithms.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingBrokenHashAlgorithms.json new file mode 100644 index 000000000..be248aafe --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingBrokenHashAlgorithms.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingBrokenHashAlgorithms", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingBrokenHashAlgorithms", + "title": "Avoid Using Broken Hash Algorithms", + "description": "Avoid using the broken algorithms MD5 or SHA-1.", + "category": "Security", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingbrokenhashalgorithms" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingCmdletAliases.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingCmdletAliases.json new file mode 100644 index 000000000..8e6e41b17 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingCmdletAliases.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingCmdletAliases", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingCmdletAliases", + "title": "Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix.", + "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, understandability and availability.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingcmdletaliases" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingComputerNameHardcoded.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingComputerNameHardcoded.json new file mode 100644 index 000000000..c42ad5940 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingComputerNameHardcoded.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingComputerNameHardcoded", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingComputerNameHardcoded", + "title": "Avoid Using ComputerName Hardcoded", + "description": "The ComputerName parameter of a cmdlet should not be hardcoded as this will expose sensitive information about the system.", + "category": "BestPractices", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingcomputernamehardcoded" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingConvertToSecureStringWithPlainText.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingConvertToSecureStringWithPlainText.json new file mode 100644 index 000000000..22de0e4f4 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingConvertToSecureStringWithPlainText.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingConvertToSecureStringWithPlainText", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingConvertToSecureStringWithPlainText", + "title": "Avoid Using SecureString With Plain Text", + "description": "Using ConvertTo-SecureString with plain text will expose secure information.", + "category": "Security", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingconverttosecurestringwithplaintext" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingDeprecatedManifestFields.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingDeprecatedManifestFields.json new file mode 100644 index 000000000..6f7d69089 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingDeprecatedManifestFields.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingDeprecatedManifestFields", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingDeprecatedManifestFields", + "title": "Avoid Using Deprecated Manifest Fields", + "description": "\"ModuleToProcess\" is obsolete in the latest PowerShell version. Please update with the latest field \"RootModule\" in manifest files to avoid PowerShell version inconsistency.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingdeprecatedmanifestfields" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingDoubleQuotesForConstantString.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingDoubleQuotesForConstantString.json new file mode 100644 index 000000000..e10c59ab7 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingDoubleQuotesForConstantString.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingDoubleQuotesForConstantString", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingDoubleQuotesForConstantString", + "title": "Avoid using double quotes if the string is constant.", + "description": "Use single quotes if the string is constant.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingdoublequotesforconstantstring" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingEmptyCatchBlock.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingEmptyCatchBlock.json new file mode 100644 index 000000000..b4277cd38 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingEmptyCatchBlock.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingEmptyCatchBlock", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingEmptyCatchBlock", + "title": "Avoid Using Empty Catch Block", + "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.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingemptycatchblock" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingInvokeExpression.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingInvokeExpression.json new file mode 100644 index 000000000..4ee0b5cb6 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingInvokeExpression.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingInvokeExpression", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingInvokeExpression", + "title": "Avoid Using Invoke-Expression", + "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 remove Invoke-Expression from script and find other options instead.", + "category": "Security", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusinginvokeexpression" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingPlainTextForPassword.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingPlainTextForPassword.json new file mode 100644 index 000000000..8ef4fce2e --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingPlainTextForPassword.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingPlainTextForPassword", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingPlainTextForPassword", + "title": "Avoid Using Plain Text For Password Parameter", + "description": "Password parameters that take in plaintext will expose passwords and compromise the security of your system.", + "category": "Security", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingplaintextforpassword" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingPositionalParameters.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingPositionalParameters.json new file mode 100644 index 000000000..faac53668 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingPositionalParameters.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingPositionalParameters", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingPositionalParameters", + "title": "Avoid Using Positional Parameters", + "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.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingpositionalparameters" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingUsernameAndPasswordParams.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingUsernameAndPasswordParams.json new file mode 100644 index 000000000..907be78fc --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingUsernameAndPasswordParams.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingUsernameAndPasswordParams", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingUsernameAndPasswordParams", + "title": "Avoid Using Username and Password Parameters", + "description": "Functions should take in a Credential parameter of type PSCredential (with a Credential transformation attribute defined after it in PowerShell 4.0 or earlier) or set the Password parameter to type SecureString.", + "category": "Security", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingusernameandpasswordparams" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingWMICmdlet.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingWMICmdlet.json new file mode 100644 index 000000000..7ac57270b --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingWMICmdlet.json @@ -0,0 +1,16 @@ +{ + "id": "PSAvoidUsingWMICmdlet", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSAvoidUsingWMICmdlet", + "title": "Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance", + "description": "Deprecated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingwmicmdlet" +} diff --git a/Analysis/Catalog/rules/powershell/PSAvoidUsingWriteHost.json b/Analysis/Catalog/rules/powershell/PSAvoidUsingWriteHost.json index 83b28ecea..93e35ac61 100644 --- a/Analysis/Catalog/rules/powershell/PSAvoidUsingWriteHost.json +++ b/Analysis/Catalog/rules/powershell/PSAvoidUsingWriteHost.json @@ -3,10 +3,14 @@ "language": "powershell", "tool": "PSScriptAnalyzer", "toolRuleId": "PSAvoidUsingWriteHost", - "title": "Avoid using Write-Host", - "description": "Encourages using Write-Output or Write-Verbose instead of Write-Host for testability and piping.", + "title": "Avoid Using Write-Host", + "description": "Avoid using the Write-Host cmdlet. Instead, use Write-Output, Write-Verbose, or Write-Information. Because Write-Host is host-specific, its implementation might vary unpredictably. Also, prior to PowerShell 5.0, Write-Host did not write to a stream, so users cannot suppress it, capture its value, or redirect it.", "category": "BestPractices", "defaultSeverity": "warning", - "tags": ["powershell", "best-practices"], + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingwritehost" } diff --git a/Analysis/Catalog/rules/powershell/PSDSCDscExamplesPresent.json b/Analysis/Catalog/rules/powershell/PSDSCDscExamplesPresent.json new file mode 100644 index 000000000..a51a4d136 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCDscExamplesPresent.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCDscExamplesPresent", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCDscExamplesPresent", + "title": "DSC examples are present", + "description": "Every DSC resource module should contain folder \"Examples\" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscdscexamplespresent" +} diff --git a/Analysis/Catalog/rules/powershell/PSDSCDscTestsPresent.json b/Analysis/Catalog/rules/powershell/PSDSCDscTestsPresent.json new file mode 100644 index 000000000..0cb7fe32d --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCDscTestsPresent.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCDscTestsPresent", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCDscTestsPresent", + "title": "Dsc tests are present", + "description": "Every DSC resource module should contain folder \"Tests\" with tests for every resource. Test scripts should have resource name they are testing in the file name.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscdsctestspresent" +} diff --git a/Analysis/Catalog/rules/powershell/PSDSCReturnCorrectTypesForDSCFunctions.json b/Analysis/Catalog/rules/powershell/PSDSCReturnCorrectTypesForDSCFunctions.json new file mode 100644 index 000000000..f7590dbc6 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCReturnCorrectTypesForDSCFunctions.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCReturnCorrectTypesForDSCFunctions", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCReturnCorrectTypesForDSCFunctions", + "title": "Return Correct Types For DSC Functions", + "description": "Set function in DSC class and Set-TargetResource in DSC resource must not return anything. Get function in DSC class must return an instance of the DSC class and Get-TargetResource function in DSC resource must return a hashtable. Test function in DSC class and Get-TargetResource function in DSC resource must return a boolean.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscreturncorrecttypesfordscfunctions" +} diff --git a/Analysis/Catalog/rules/powershell/PSDSCStandardDSCFunctionsInResource.json b/Analysis/Catalog/rules/powershell/PSDSCStandardDSCFunctionsInResource.json new file mode 100644 index 000000000..9a1b93b96 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCStandardDSCFunctionsInResource.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCStandardDSCFunctionsInResource", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCStandardDSCFunctionsInResource", + "title": "Use Standard Get/Set/Test TargetResource functions in DSC Resource", + "description": "DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions.", + "category": "BestPractices", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscstandarddscfunctionsinresource" +} diff --git a/Analysis/Catalog/rules/powershell/PSDSCUseIdenticalMandatoryParametersForDSC.json b/Analysis/Catalog/rules/powershell/PSDSCUseIdenticalMandatoryParametersForDSC.json new file mode 100644 index 000000000..38566c377 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCUseIdenticalMandatoryParametersForDSC.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCUseIdenticalMandatoryParametersForDSC", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCUseIdenticalMandatoryParametersForDSC", + "title": "Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions", + "description": "The Get/Test/Set TargetResource functions of DSC resource must have the same mandatory parameters.", + "category": "BestPractices", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscuseidenticalmandatoryparametersfordsc" +} diff --git a/Analysis/Catalog/rules/powershell/PSDSCUseIdenticalParametersForDSC.json b/Analysis/Catalog/rules/powershell/PSDSCUseIdenticalParametersForDSC.json new file mode 100644 index 000000000..e0894e6e5 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCUseIdenticalParametersForDSC.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCUseIdenticalParametersForDSC", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCUseIdenticalParametersForDSC", + "title": "Use Identical Parameters For DSC Test and Set Functions", + "description": "The Test and Set-TargetResource functions of DSC Resource must have the same parameters.", + "category": "BestPractices", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscuseidenticalparametersfordsc" +} diff --git a/Analysis/Catalog/rules/powershell/PSDSCUseVerboseMessageInDSCResource.json b/Analysis/Catalog/rules/powershell/PSDSCUseVerboseMessageInDSCResource.json new file mode 100644 index 000000000..053f8caed --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSDSCUseVerboseMessageInDSCResource.json @@ -0,0 +1,16 @@ +{ + "id": "PSDSCUseVerboseMessageInDSCResource", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSDSCUseVerboseMessageInDSCResource", + "title": "Use verbose message in DSC resource", + "description": "It is a best practice to emit informative, verbose messages in DSC resource functions. This helps in debugging issues when a DSC configuration is executed.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/dscuseverbosemessageindscresource" +} diff --git a/Analysis/Catalog/rules/powershell/PSMisleadingBacktick.json b/Analysis/Catalog/rules/powershell/PSMisleadingBacktick.json new file mode 100644 index 000000000..c48298c8c --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSMisleadingBacktick.json @@ -0,0 +1,16 @@ +{ + "id": "PSMisleadingBacktick", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSMisleadingBacktick", + "title": "Misleading Backtick", + "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.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/misleadingbacktick" +} diff --git a/Analysis/Catalog/rules/powershell/PSMissingModuleManifestField.json b/Analysis/Catalog/rules/powershell/PSMissingModuleManifestField.json new file mode 100644 index 000000000..94795483f --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSMissingModuleManifestField.json @@ -0,0 +1,16 @@ +{ + "id": "PSMissingModuleManifestField", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSMissingModuleManifestField", + "title": "Module Manifest Fields", + "description": "Some fields of the module manifest (such as ModuleVersion) are required.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/missingmodulemanifestfield" +} diff --git a/Analysis/Catalog/rules/powershell/PSPlaceCloseBrace.json b/Analysis/Catalog/rules/powershell/PSPlaceCloseBrace.json new file mode 100644 index 000000000..ef886d3d7 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSPlaceCloseBrace.json @@ -0,0 +1,16 @@ +{ + "id": "PSPlaceCloseBrace", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSPlaceCloseBrace", + "title": "Place close braces", + "description": "Close brace should be on a new line by itself.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/placeclosebrace" +} diff --git a/Analysis/Catalog/rules/powershell/PSPlaceOpenBrace.json b/Analysis/Catalog/rules/powershell/PSPlaceOpenBrace.json new file mode 100644 index 000000000..7b8f3eb72 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSPlaceOpenBrace.json @@ -0,0 +1,16 @@ +{ + "id": "PSPlaceOpenBrace", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSPlaceOpenBrace", + "title": "Place open braces consistently", + "description": "Place open braces either on the same line as the preceding expression or on a new line.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/placeopenbrace" +} diff --git a/Analysis/Catalog/rules/powershell/PSPossibleIncorrectComparisonWithNull.json b/Analysis/Catalog/rules/powershell/PSPossibleIncorrectComparisonWithNull.json new file mode 100644 index 000000000..93f225421 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSPossibleIncorrectComparisonWithNull.json @@ -0,0 +1,16 @@ +{ + "id": "PSPossibleIncorrectComparisonWithNull", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSPossibleIncorrectComparisonWithNull", + "title": "Possible Incorrect Comparison With Null", + "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 whether 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.", + "category": "Reliability", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "reliability" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/possibleincorrectcomparisonwithnull" +} diff --git a/Analysis/Catalog/rules/powershell/PSPossibleIncorrectUsageOfAssignmentOperator.json b/Analysis/Catalog/rules/powershell/PSPossibleIncorrectUsageOfAssignmentOperator.json new file mode 100644 index 000000000..f2e0cb1d9 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSPossibleIncorrectUsageOfAssignmentOperator.json @@ -0,0 +1,16 @@ +{ + "id": "PSPossibleIncorrectUsageOfAssignmentOperator", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSPossibleIncorrectUsageOfAssignmentOperator", + "title": "'=' is not an assignment operator. Did you mean the equality operator '-eq'?", + "description": "'=' or '==' are not comparison operators in the PowerShell language and rarely needed inside conditional statements.", + "category": "Reliability", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "reliability" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/possibleincorrectusageofassignmentoperator" +} diff --git a/Analysis/Catalog/rules/powershell/PSPossibleIncorrectUsageOfRedirectionOperator.json b/Analysis/Catalog/rules/powershell/PSPossibleIncorrectUsageOfRedirectionOperator.json new file mode 100644 index 000000000..6b64ea74b --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSPossibleIncorrectUsageOfRedirectionOperator.json @@ -0,0 +1,16 @@ +{ + "id": "PSPossibleIncorrectUsageOfRedirectionOperator", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSPossibleIncorrectUsageOfRedirectionOperator", + "title": "'>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal).", + "description": "When switching between different languages it is easy to forget that '>' does not mean 'greater than' in PowerShell.", + "category": "Reliability", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "reliability" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/possibleincorrectusageofredirectionoperator" +} diff --git a/Analysis/Catalog/rules/powershell/PSProvideCommentHelp.json b/Analysis/Catalog/rules/powershell/PSProvideCommentHelp.json new file mode 100644 index 000000000..e5690e77b --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSProvideCommentHelp.json @@ -0,0 +1,16 @@ +{ + "id": "PSProvideCommentHelp", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSProvideCommentHelp", + "title": "Basic Comment Help", + "description": "Checks that all cmdlets have a help comment. This rule only checks existence. It does not check the content of the comment.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/providecommenthelp" +} diff --git a/Analysis/Catalog/rules/powershell/PSReservedCmdletChar.json b/Analysis/Catalog/rules/powershell/PSReservedCmdletChar.json new file mode 100644 index 000000000..53c7f55cd --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSReservedCmdletChar.json @@ -0,0 +1,16 @@ +{ + "id": "PSReservedCmdletChar", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSReservedCmdletChar", + "title": "Reserved Cmdlet Chars", + "description": "Checks for reserved characters in cmdlet names. These characters usually cause a parsing error. Otherwise they will generally cause runtime errors.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/reservedcmdletchar" +} diff --git a/Analysis/Catalog/rules/powershell/PSReservedParams.json b/Analysis/Catalog/rules/powershell/PSReservedParams.json new file mode 100644 index 000000000..37dbc79eb --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSReservedParams.json @@ -0,0 +1,16 @@ +{ + "id": "PSReservedParams", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSReservedParams", + "title": "Reserved Parameters", + "description": "Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs.", + "category": "BestPractices", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/reservedparams" +} diff --git a/Analysis/Catalog/rules/powershell/PSReviewUnusedParameter.json b/Analysis/Catalog/rules/powershell/PSReviewUnusedParameter.json new file mode 100644 index 000000000..51ca0631f --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSReviewUnusedParameter.json @@ -0,0 +1,16 @@ +{ + "id": "PSReviewUnusedParameter", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSReviewUnusedParameter", + "title": "ReviewUnusedParameter", + "description": "Ensure all parameters are used within the same script, scriptblock, or function where they are declared.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/reviewunusedparameter" +} diff --git a/Analysis/Catalog/rules/powershell/PSShouldProcess.json b/Analysis/Catalog/rules/powershell/PSShouldProcess.json new file mode 100644 index 000000000..9a91111c3 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSShouldProcess.json @@ -0,0 +1,16 @@ +{ + "id": "PSShouldProcess", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSShouldProcess", + "title": "Should Process", + "description": "Checks that if the SupportsShouldProcess is present, the function calls ShouldProcess/ShouldContinue and vice versa. Scripts with one or the other but not both will generally run into an error or unexpected behavior.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/shouldprocess" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseApprovedVerbs.json b/Analysis/Catalog/rules/powershell/PSUseApprovedVerbs.json new file mode 100644 index 000000000..9ef495423 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseApprovedVerbs.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseApprovedVerbs", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseApprovedVerbs", + "title": "Cmdlet Verbs", + "description": "Checks that all defined cmdlets use approved verbs. This is in line with PowerShell's best practices.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useapprovedverbs" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseBOMForUnicodeEncodedFile.json b/Analysis/Catalog/rules/powershell/PSUseBOMForUnicodeEncodedFile.json new file mode 100644 index 000000000..b88e4ece5 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseBOMForUnicodeEncodedFile.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseBOMForUnicodeEncodedFile", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseBOMForUnicodeEncodedFile", + "title": "Use BOM encoding for non-ASCII files", + "description": "For a file encoded with a format other than ASCII, ensure BOM is present to ensure that any application consuming this file can interpret it correctly.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usebomforunicodeencodedfile" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseCmdletCorrectly.json b/Analysis/Catalog/rules/powershell/PSUseCmdletCorrectly.json new file mode 100644 index 000000000..cdbefa850 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseCmdletCorrectly.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseCmdletCorrectly", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseCmdletCorrectly", + "title": "Use Cmdlet Correctly", + "description": "Cmdlet should be called with the mandatory parameters.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usecmdletcorrectly" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseCompatibleCmdlets.json b/Analysis/Catalog/rules/powershell/PSUseCompatibleCmdlets.json new file mode 100644 index 000000000..eff499bb5 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseCompatibleCmdlets.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseCompatibleCmdlets", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseCompatibleCmdlets", + "title": "Use compatible cmdlets", + "description": "Use cmdlets compatible with the given PowerShell version and edition and operating system.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usecompatiblecmdlets" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseCompatibleCommands.json b/Analysis/Catalog/rules/powershell/PSUseCompatibleCommands.json new file mode 100644 index 000000000..c487bb15f --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseCompatibleCommands.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseCompatibleCommands", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseCompatibleCommands", + "title": "Use compatible commands", + "description": "Use commands compatible with the given PowerShell version and operating system.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usecompatiblecommands" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseCompatibleSyntax.json b/Analysis/Catalog/rules/powershell/PSUseCompatibleSyntax.json new file mode 100644 index 000000000..34a47682d --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseCompatibleSyntax.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseCompatibleSyntax", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseCompatibleSyntax", + "title": "Use compatible syntax", + "description": "Use script syntax compatible with the given PowerShell versions.", + "category": "BestPractices", + "defaultSeverity": "error", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usecompatiblesyntax" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseCompatibleTypes.json b/Analysis/Catalog/rules/powershell/PSUseCompatibleTypes.json new file mode 100644 index 000000000..8f336d43d --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseCompatibleTypes.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseCompatibleTypes", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseCompatibleTypes", + "title": "Use compatible types", + "description": "Use types compatible with the given PowerShell version and operating system.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usecompatibletypes" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseConsistentIndentation.json b/Analysis/Catalog/rules/powershell/PSUseConsistentIndentation.json new file mode 100644 index 000000000..cc9d6eb1c --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseConsistentIndentation.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseConsistentIndentation", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseConsistentIndentation", + "title": "Use consistent indentation", + "description": "Each statement block should have a consistent indentation.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useconsistentindentation" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseConsistentWhitespace.json b/Analysis/Catalog/rules/powershell/PSUseConsistentWhitespace.json new file mode 100644 index 000000000..13a06f583 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseConsistentWhitespace.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseConsistentWhitespace", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseConsistentWhitespace", + "title": "Use Consistent Whitespace", + "description": "Check for whitespace between keyword and open paren/curly, around assignment operator ('='), around arithmetic operators and after separators (',' and ';').", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useconsistentwhitespace" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseCorrectCasing.json b/Analysis/Catalog/rules/powershell/PSUseCorrectCasing.json new file mode 100644 index 000000000..e2c0a9787 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseCorrectCasing.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseCorrectCasing", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseCorrectCasing", + "title": "Use exact casing of cmdlet/function/parameter name.", + "description": "For better readability and consistency, use consistent casing.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usecorrectcasing" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseDeclaredVarsMoreThanAssignments.json b/Analysis/Catalog/rules/powershell/PSUseDeclaredVarsMoreThanAssignments.json new file mode 100644 index 000000000..1c29a2d43 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseDeclaredVarsMoreThanAssignments.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseDeclaredVarsMoreThanAssignments", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseDeclaredVarsMoreThanAssignments", + "title": "Extra Variables", + "description": "Ensure declared variables are used elsewhere in the script and not just during assignment.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usedeclaredvarsmorethanassignments" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseLiteralInitializerForHashtable.json b/Analysis/Catalog/rules/powershell/PSUseLiteralInitializerForHashtable.json new file mode 100644 index 000000000..2a91f9734 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseLiteralInitializerForHashtable.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseLiteralInitializerForHashtable", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseLiteralInitializerForHashtable", + "title": "Create hashtables with literal initializers", + "description": "Use literal initializer, @{}, for creating a hashtable as they are case-insensitive by default.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useliteralinitializerforhashtable" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseOutputTypeCorrectly.json b/Analysis/Catalog/rules/powershell/PSUseOutputTypeCorrectly.json new file mode 100644 index 000000000..7467c3fa3 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseOutputTypeCorrectly.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseOutputTypeCorrectly", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseOutputTypeCorrectly", + "title": "Use OutputType Correctly", + "description": "The return types of a cmdlet should be declared using the OutputType attribute.", + "category": "BestPractices", + "defaultSeverity": "info", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useoutputtypecorrectly" +} diff --git a/Analysis/Catalog/rules/powershell/PSUsePSCredentialType.json b/Analysis/Catalog/rules/powershell/PSUsePSCredentialType.json new file mode 100644 index 000000000..8c896734d --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUsePSCredentialType.json @@ -0,0 +1,16 @@ +{ + "id": "PSUsePSCredentialType", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUsePSCredentialType", + "title": "Use PSCredential type.", + "description": "For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute.", + "category": "Security", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "security" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usepscredentialtype" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseProcessBlockForPipelineCommand.json b/Analysis/Catalog/rules/powershell/PSUseProcessBlockForPipelineCommand.json new file mode 100644 index 000000000..fbcce65f9 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseProcessBlockForPipelineCommand.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseProcessBlockForPipelineCommand", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseProcessBlockForPipelineCommand", + "title": "Use process block for command that accepts input from pipeline.", + "description": "If a command parameter takes its value from the pipeline, the command must use a process block to bind the input objects from the pipeline to that parameter.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useprocessblockforpipelinecommand" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseShouldProcessForStateChangingFunctions.json b/Analysis/Catalog/rules/powershell/PSUseShouldProcessForStateChangingFunctions.json new file mode 100644 index 000000000..5d6178b66 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseShouldProcessForStateChangingFunctions.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseShouldProcessForStateChangingFunctions", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseShouldProcessForStateChangingFunctions", + "title": "Use ShouldProcess For State Changing Functions", + "description": "Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useshouldprocessforstatechangingfunctions" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseSingularNouns.json b/Analysis/Catalog/rules/powershell/PSUseSingularNouns.json new file mode 100644 index 000000000..76de4b2fc --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseSingularNouns.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseSingularNouns", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseSingularNouns", + "title": "Cmdlet Singular Noun", + "description": "Cmdlet should use singular instead of plural nouns.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usesingularnouns" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseSupportsShouldProcess.json b/Analysis/Catalog/rules/powershell/PSUseSupportsShouldProcess.json new file mode 100644 index 000000000..2ca8b6365 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseSupportsShouldProcess.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseSupportsShouldProcess", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseSupportsShouldProcess", + "title": "Use SupportsShouldProcess", + "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.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usesupportsshouldprocess" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseToExportFieldsInManifest.json b/Analysis/Catalog/rules/powershell/PSUseToExportFieldsInManifest.json new file mode 100644 index 000000000..56600f394 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseToExportFieldsInManifest.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseToExportFieldsInManifest", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseToExportFieldsInManifest", + "title": "Use the *ToExport module manifest fields.", + "description": "in a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/usetoexportfieldsinmanifest" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseUTF8EncodingForHelpFile.json b/Analysis/Catalog/rules/powershell/PSUseUTF8EncodingForHelpFile.json new file mode 100644 index 000000000..1db9507a8 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseUTF8EncodingForHelpFile.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseUTF8EncodingForHelpFile", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseUTF8EncodingForHelpFile", + "title": "Use UTF8 Encoding For Help File", + "description": "PowerShell help file needs to use UTF8 Encoding.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useutf8encodingforhelpfile" +} diff --git a/Analysis/Catalog/rules/powershell/PSUseUsingScopeModifierInNewRunspaces.json b/Analysis/Catalog/rules/powershell/PSUseUsingScopeModifierInNewRunspaces.json new file mode 100644 index 000000000..01ccbc1c6 --- /dev/null +++ b/Analysis/Catalog/rules/powershell/PSUseUsingScopeModifierInNewRunspaces.json @@ -0,0 +1,16 @@ +{ + "id": "PSUseUsingScopeModifierInNewRunspaces", + "language": "powershell", + "tool": "PSScriptAnalyzer", + "toolRuleId": "PSUseUsingScopeModifierInNewRunspaces", + "title": "Use 'Using:' scope modifier in RunSpace ScriptBlocks", + "description": "If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock.", + "category": "BestPractices", + "defaultSeverity": "warning", + "tags": [ + "powershell", + "psscriptanalyzer", + "best-practices" + ], + "docs": "https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/useusingscopemodifierinnewrunspaces" +} diff --git a/Analysis/Packs/README.md b/Analysis/Packs/README.md index c3e198fbf..f50110a45 100644 --- a/Analysis/Packs/README.md +++ b/Analysis/Packs/README.md @@ -17,6 +17,7 @@ Packs are curated rule sets built from rule IDs and optional includes/overrides. - `intelligencex-maintainability-default` - Security defaults: - `csharp-security-default` + - `powershell-security-default` - `all-security-default` - Language tiers: - `csharp-50`, `csharp-100`, `csharp-500` @@ -43,3 +44,9 @@ For C#, tiers are generated from built-in NetAnalyzers metadata: Regenerate C# catalog + C# tiers with: `./scripts/update_analysis_catalog.py --repo-root .` + +## PowerShell catalog + +PowerShell rules are sourced from `PSScriptAnalyzer` rule metadata. To (re)generate the built-in PowerShell rule catalog: + +`pwsh -NoProfile -File ./scripts/sync-pssa-catalog.ps1` diff --git a/Analysis/Packs/all-security-default.json b/Analysis/Packs/all-security-default.json index 00da6a598..51ed91c81 100644 --- a/Analysis/Packs/all-security-default.json +++ b/Analysis/Packs/all-security-default.json @@ -1,8 +1,7 @@ { "id": "all-security-default", "label": "All Security (Default)", - "description": "Cross-language security-focused pack. Currently includes C# security rules.", - "includes": ["csharp-security-default"], + "description": "Cross-language security-focused pack.", + "includes": ["csharp-security-default", "powershell-security-default"], "rules": [] } - diff --git a/Analysis/Packs/powershell-default.json b/Analysis/Packs/powershell-default.json index 38c354bbf..9ece70f84 100644 --- a/Analysis/Packs/powershell-default.json +++ b/Analysis/Packs/powershell-default.json @@ -1,6 +1,16 @@ { "id": "powershell-default", "label": "PowerShell Default", - "description": "Core best-practice rules for PowerShell scripts.", - "rules": ["PSAvoidUsingWriteHost"] + "description": "High-signal security and correctness rules for PowerShell scripts (PSScriptAnalyzer).", + "includes": ["powershell-security-default"], + "rules": [ + "PSAvoidOverwritingBuiltInCmdlets", + "PSAvoidUsingCmdletAliases", + "PSAvoidUsingEmptyCatchBlock", + "PSAvoidUsingWriteHost", + "PSPossibleIncorrectComparisonWithNull", + "PSPossibleIncorrectUsageOfAssignmentOperator", + "PSPossibleIncorrectUsageOfRedirectionOperator", + "PSReviewUnusedParameter" + ] } diff --git a/Analysis/Packs/powershell-security-default.json b/Analysis/Packs/powershell-security-default.json new file mode 100644 index 000000000..c01961cdf --- /dev/null +++ b/Analysis/Packs/powershell-security-default.json @@ -0,0 +1,15 @@ +{ + "id": "powershell-security-default", + "label": "PowerShell Security (Default)", + "description": "PowerShell security-focused pack (PSScriptAnalyzer).", + "includes": [], + "rules": [ + "PSAvoidUsingAllowUnencryptedAuthentication", + "PSAvoidUsingBrokenHashAlgorithms", + "PSAvoidUsingConvertToSecureStringWithPlainText", + "PSAvoidUsingInvokeExpression", + "PSAvoidUsingPlainTextForPassword", + "PSAvoidUsingUsernameAndPasswordParams", + "PSUsePSCredentialType" + ] +} diff --git a/IntelligenceX.Tests/Program.Reviewer.AnalysisCatalogAndPolicy.cs b/IntelligenceX.Tests/Program.Reviewer.AnalysisCatalogAndPolicy.cs index 880a1b905..0a0361dd4 100644 --- a/IntelligenceX.Tests/Program.Reviewer.AnalysisCatalogAndPolicy.cs +++ b/IntelligenceX.Tests/Program.Reviewer.AnalysisCatalogAndPolicy.cs @@ -307,6 +307,260 @@ private static void TestAnalysisCatalogRuleOverridesApply() { } } + private static void TestAnalysisCatalogPowerShellOverridesApply() { + var workspace = ResolveWorkspaceRoot(); + // Hermetic: validates checked-in catalog JSON/overrides only (does not invoke PSScriptAnalyzer or the sync script). + var catalog = IntelligenceX.Analysis.AnalysisCatalogLoader.LoadFromWorkspace(workspace); + + var rulesDir = Path.Combine(workspace, "Analysis", "Catalog", "rules", "powershell"); + var overridesDir = Path.Combine(workspace, "Analysis", "Catalog", "overrides", "powershell"); + AssertEqual(true, Directory.Exists(overridesDir), "powershell overrides dir exists"); + + // Load the catalog without overrides so we can compare base vs effective without needing per-override temp workspaces. + var rulesRoot = Path.Combine(workspace, "Analysis", "Catalog", "rules"); + var packsRoot = Path.Combine(workspace, "Analysis", "Packs"); + var explicitOverridesRoot = Path.Combine(workspace, "Analysis", "Catalog", "overrides"); + var effectiveCatalogFromPaths = IntelligenceX.Analysis.AnalysisCatalogLoader.LoadFromPaths( + rulesRoot, + explicitOverridesRoot, + packsRoot); + + // Avoid creating/deleting temp directories (flake risk on Windows). Passing a non-existent overrides root + // is sufficient to ensure no overrides are applied. + var missingOverridesRoot = Path.Combine(Path.GetTempPath(), "ix-analysis-missing-overrides-" + Guid.NewGuid().ToString("N")); + AssertEqual(false, Directory.Exists(missingOverridesRoot), "missing overrides root does not exist"); + var baseCatalog = IntelligenceX.Analysis.AnalysisCatalogLoader.LoadFromPaths( + rulesRoot, + missingOverridesRoot, + packsRoot); + + foreach (var overridePath in Directory.EnumerateFiles(overridesDir, "*.json")) { + var overrideText = File.ReadAllText(overridePath, System.Text.Encoding.UTF8); + using var overrideDoc = System.Text.Json.JsonDocument.Parse(overrideText); + var overrideRoot = overrideDoc.RootElement; + + if (!overrideRoot.TryGetProperty("id", out var idElement) || idElement.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new InvalidOperationException($"Override '{Path.GetFileName(overridePath)}' is missing string 'id' property."); + } + + var id = idElement.GetString(); + AssertEqual(false, string.IsNullOrWhiteSpace(id), $"{Path.GetFileName(overridePath)} override has id"); + if (string.IsNullOrWhiteSpace(id)) { + throw new Exception($"{Path.GetFileName(overridePath)} override has no id"); + } + AssertEqual(id, Path.GetFileNameWithoutExtension(overridePath), $"{id} override filename matches id"); + + var basePath = Path.Combine(rulesDir, id + ".json"); + AssertEqual(true, File.Exists(basePath), $"{id} base rule exists for override"); + + AssertEqual(true, catalog.Rules.TryGetValue(id, out var effective), $"{id} exists in catalog"); + if (effective is null) { + throw new Exception($"{id} exists in catalog but is null"); + } + + AssertEqual(true, effectiveCatalogFromPaths.Rules.TryGetValue(id, out var effectiveFromPaths), $"{id} exists in explicit-overrides catalog"); + if (effectiveFromPaths is null) { + throw new Exception($"{id} exists in explicit-overrides catalog but is null"); + } + AssertEqual(effective.Title, effectiveFromPaths.Title, $"{id} explicit-overrides title matches workspace loader"); + AssertEqual(effective.Description, effectiveFromPaths.Description, $"{id} explicit-overrides description matches workspace loader"); + + AssertEqual(true, baseCatalog.Rules.TryGetValue(id, out var resolvedBase), $"{id} exists in base catalog"); + var baseRule = resolvedBase ?? throw new Exception($"{id} exists in base catalog but is null"); + + // Ensure our "base catalog" truly reflects the rule JSON without applying any overrides. + var baseText = File.ReadAllText(basePath, System.Text.Encoding.UTF8); + using (var baseDoc = System.Text.Json.JsonDocument.Parse(baseText)) { + var baseRoot = baseDoc.RootElement; + + AssertEqual(true, baseRoot.TryGetProperty("title", out var baseTitle), $"{id} base rule json has 'title'"); + AssertEqual(System.Text.Json.JsonValueKind.String, baseTitle.ValueKind, $"{id} base rule json title is string"); + + AssertEqual(true, baseRoot.TryGetProperty("description", out var baseDescription), $"{id} base rule json has 'description'"); + AssertEqual(System.Text.Json.JsonValueKind.String, baseDescription.ValueKind, $"{id} base rule json description is string"); + + AssertEqual(baseTitle.GetString(), baseRule.Title, $"{id} base title matches rule json"); + AssertEqual(baseDescription.GetString(), baseRule.Description, $"{id} base description matches rule json"); + } + + // Validate override schema and verify that each overridden value is reflected in the effective rule. + // Keep this block inside the per-override loop so each file is validated independently. + var sawSupportedOverrideProperty = false; + var changesBase = false; + foreach (var prop in overrideRoot.EnumerateObject()) { + if (prop.NameEquals("id")) { + continue; + } + + switch (prop.Name) { + case "title": { + sawSupportedOverrideProperty = true; + if (prop.Value.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new Exception($"{id} override title must be a string"); + } + var expectedRaw = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(expectedRaw)) { + // ApplyOverride treats whitespace as "no override". + AssertEqual(baseRule.Title, effective.Title, $"{id} override title blank/no-op"); + break; + } + var expected = expectedRaw; + AssertEqual(expected, effective.Title, $"{id} override title applied"); + if (!string.Equals(expected, baseRule.Title, StringComparison.Ordinal)) { + changesBase = true; + } + break; + } + case "description": { + sawSupportedOverrideProperty = true; + if (prop.Value.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new Exception($"{id} override description must be a string"); + } + var expectedRaw = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(expectedRaw)) { + // ApplyOverride treats whitespace as "no override". + AssertEqual(baseRule.Description, effective.Description, $"{id} override description blank/no-op"); + break; + } + var expected = expectedRaw; + AssertEqual(expected, effective.Description, $"{id} override description applied"); + if (!string.Equals(expected, baseRule.Description, StringComparison.Ordinal)) { + changesBase = true; + } + break; + } + case "type": { + sawSupportedOverrideProperty = true; + if (prop.Value.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new Exception($"{id} override type must be a string"); + } + var expectedRaw = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(expectedRaw)) { + // ApplyOverride treats whitespace as "no override". + AssertEqual(baseRule.Type, effective.Type, $"{id} override type blank/no-op"); + break; + } + var expected = expectedRaw; + AssertEqual(expected, effective.Type, $"{id} override type applied"); + if (!string.Equals(expected, baseRule.Type, StringComparison.Ordinal)) { + changesBase = true; + } + break; + } + case "category": { + sawSupportedOverrideProperty = true; + if (prop.Value.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new Exception($"{id} override category must be a string"); + } + var expectedRaw = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(expectedRaw)) { + // ApplyOverride treats whitespace as "no override". + AssertEqual(baseRule.Category, effective.Category, $"{id} override category blank/no-op"); + break; + } + var expected = expectedRaw; + AssertEqual(expected, effective.Category, $"{id} override category applied"); + if (!string.Equals(expected, baseRule.Category, StringComparison.Ordinal)) { + changesBase = true; + } + break; + } + case "defaultSeverity": { + sawSupportedOverrideProperty = true; + if (prop.Value.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new Exception($"{id} override defaultSeverity must be a string"); + } + var expectedRaw = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(expectedRaw)) { + // ApplyOverride treats whitespace as "no override". + AssertEqual(baseRule.DefaultSeverity, effective.DefaultSeverity, $"{id} override defaultSeverity blank/no-op"); + break; + } + var expected = expectedRaw; + AssertEqual(expected, effective.DefaultSeverity, $"{id} override defaultSeverity applied"); + if (!string.Equals(expected, baseRule.DefaultSeverity, StringComparison.Ordinal)) { + changesBase = true; + } + break; + } + case "docs": { + sawSupportedOverrideProperty = true; + if (prop.Value.ValueKind != System.Text.Json.JsonValueKind.String) { + throw new Exception($"{id} override docs must be a string"); + } + var expectedRaw = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(expectedRaw)) { + // ApplyOverride treats whitespace as "no override". + AssertEqual(baseRule.Docs, effective.Docs, $"{id} override docs blank/no-op"); + break; + } + var expected = expectedRaw; + AssertEqual(expected, effective.Docs, $"{id} override docs applied"); + if (!string.Equals(expected, baseRule.Docs, StringComparison.Ordinal)) { + changesBase = true; + } + break; + } + case "tags": { + sawSupportedOverrideProperty = true; + + static System.Collections.Generic.IReadOnlyList MergeTags( + System.Collections.Generic.IReadOnlyList existing, + System.Collections.Generic.IReadOnlyList overrides) { + var set = new HashSet(StringComparer.OrdinalIgnoreCase); + var merged = new List(); + foreach (var tag in existing ?? Array.Empty()) { + if (string.IsNullOrWhiteSpace(tag)) { + continue; + } + var value = tag.Trim(); + if (set.Add(value)) { + merged.Add(value); + } + } + foreach (var tag in overrides ?? Array.Empty()) { + if (string.IsNullOrWhiteSpace(tag)) { + continue; + } + var value = tag.Trim(); + if (set.Add(value)) { + merged.Add(value); + } + } + return merged; + } + + AssertEqual(System.Text.Json.JsonValueKind.Array, prop.Value.ValueKind, $"{id} override tags is array"); + var overrideTags = prop.Value.EnumerateArray() + .Select(x => x.ValueKind == System.Text.Json.JsonValueKind.String + ? x.GetString()! + : throw new Exception($"{id} override tags entries must be strings")) + .ToArray(); + + var expectedMerged = MergeTags(baseRule.Tags ?? Array.Empty(), overrideTags); + var expectedSet = new HashSet(expectedMerged, StringComparer.OrdinalIgnoreCase); + var actualSet = new HashSet(effective.Tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + AssertEqual(true, expectedSet.SetEquals(actualSet), $"{id} merged tags set equals"); + var baseSet = new HashSet(baseRule.Tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + if (!baseSet.SetEquals(actualSet)) { + changesBase = true; + } + break; + } + default: + // Production currently ignores unknown override properties; fail fast in tests so typos + // (e.g., "defualtSeverity") don't silently make overrides ineffective. + throw new Exception($"{id} override has unsupported property '{prop.Name}'."); + } + } + + AssertEqual(true, sawSupportedOverrideProperty, $"{id} override has at least one supported property besides id"); + + // No-op overrides are allowed: they can document intent, normalize text, or future-proof rule metadata. + _ = changesBase; + } + } + private static void TestAnalysisCatalogOverrideInvalidTypeFallsBack() { var temp = Path.Combine(Path.GetTempPath(), "ix-analysis-overrides-invalid-type-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(temp); diff --git a/IntelligenceX.Tests/Program.Reviewer.AnalysisDocs.cs b/IntelligenceX.Tests/Program.Reviewer.AnalysisDocs.cs new file mode 100644 index 000000000..013df1301 --- /dev/null +++ b/IntelligenceX.Tests/Program.Reviewer.AnalysisDocs.cs @@ -0,0 +1,73 @@ +namespace IntelligenceX.Tests; + +#if INTELLIGENCEX_REVIEWER +internal static partial class Program { + private static void TestAnalysisCatalogPowerShellDocsLinksMatchLearnPattern() { + var workspace = ResolveWorkspaceRoot(); + var catalog = IntelligenceX.Analysis.AnalysisCatalogLoader.LoadFromWorkspace(workspace); + + static bool LooksLikeLearnLocaleSegment(string? value) { + // Example: en-us, de-de. We keep this permissive (just shape) so the test doesn't need a locale allowlist. + if (string.IsNullOrWhiteSpace(value) || value!.Length != 5) { + return false; + } + return char.IsLetter(value[0]) && + char.IsLetter(value[1]) && + value[2] == '-' && + char.IsLetter(value[3]) && + char.IsLetter(value[4]); + } + + foreach (var entry in catalog.Rules) { + var rule = entry.Value; + if (!string.Equals(rule.Language, "powershell", StringComparison.OrdinalIgnoreCase)) { + continue; + } + if (!string.Equals(rule.Tool, "PSScriptAnalyzer", StringComparison.OrdinalIgnoreCase)) { + continue; + } + var docsRaw = rule.Docs; + AssertEqual(false, string.IsNullOrWhiteSpace(docsRaw), $"{rule.Id} docs is populated"); + if (string.IsNullOrWhiteSpace(docsRaw)) { + throw new InvalidOperationException($"{rule.Id} docs is populated"); + } + + var docs = docsRaw.Trim(); + AssertEqual(false, docs.Any(char.IsWhiteSpace), $"{rule.Id} docs has no whitespace"); + AssertEqual(true, Uri.IsWellFormedUriString(docs, UriKind.Absolute), $"{rule.Id} docs is well-formed"); + if (!Uri.TryCreate(docs, UriKind.Absolute, out var uri) || uri is null) { + throw new InvalidOperationException($"Expected {rule.Id} docs to be a valid absolute url, got '{docs}'."); + } + AssertEqual("https", uri.Scheme, $"{rule.Id} docs uses https"); + + // Prefer Learn; tolerate harmless canonicalization differences like subdomains. + var hostOk = + uri.Host.Equals("learn.microsoft.com", StringComparison.OrdinalIgnoreCase) || + uri.Host.EndsWith(".learn.microsoft.com", StringComparison.OrdinalIgnoreCase); + AssertEqual(true, hostOk, $"{rule.Id} docs host is Learn"); + + // Ignore harmless URL canonicalization differences (e.g. query strings on docs URLs). + var normalizedUri = new UriBuilder(uri) { Query = "", Fragment = "" }.Uri; + var path = normalizedUri.AbsolutePath; + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + var offset = 0; + // Learn sometimes includes a locale segment, e.g. /en-us/powershell/... + if (segments.Length > 0 && LooksLikeLearnLocaleSegment(segments[0])) { + offset = 1; + } + AssertEqual(true, segments.Length >= (5 + offset), $"{rule.Id} docs path has enough segments"); + if (segments.Length < (5 + offset)) { + throw new InvalidOperationException($"Expected {rule.Id} docs path to include '/powershell/utility-modules/psscriptanalyzer/rules/', got '{path}'."); + } + AssertEqual("powershell", segments[offset + 0], $"{rule.Id} docs uses Learn powershell path"); + AssertEqual("utility-modules", segments[offset + 1], $"{rule.Id} docs uses Learn utility-modules path"); + AssertEqual("psscriptanalyzer", segments[offset + 2], $"{rule.Id} docs uses Learn PSScriptAnalyzer path"); + AssertEqual("rules", segments[offset + 3], $"{rule.Id} docs uses Learn rules path"); + + var actualSlug = segments[offset + 4].Trim('/'); + AssertEqual(false, string.IsNullOrWhiteSpace(actualSlug), $"{rule.Id} docs slug is present"); + AssertEqual(false, actualSlug.Any(char.IsWhiteSpace), $"{rule.Id} docs slug has no whitespace"); + } + } +} +#endif diff --git a/IntelligenceX.Tests/Program.Reviewer.AnalysisPacks.BuiltIn.cs b/IntelligenceX.Tests/Program.Reviewer.AnalysisPacks.BuiltIn.cs new file mode 100644 index 000000000..d9a613a30 --- /dev/null +++ b/IntelligenceX.Tests/Program.Reviewer.AnalysisPacks.BuiltIn.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; + +namespace IntelligenceX.Tests; + +#if INTELLIGENCEX_REVIEWER +internal static partial class Program { + private static void TestAnalysisPacksAllSecurityIncludesPowerShell() { + var workspace = ResolveWorkspaceRoot(); + var catalog = IntelligenceX.Analysis.AnalysisCatalogLoader.LoadFromWorkspace(workspace); + + AssertEqual(true, catalog.Packs.ContainsKey("all-security-default"), "pack all-security-default exists"); + AssertEqual(true, catalog.Packs.ContainsKey("powershell-security-default"), "pack powershell-security-default exists"); + AssertEqual(true, catalog.Packs.ContainsKey("csharp-security-default"), "pack csharp-security-default exists"); + + var allSecurity = catalog.Packs["all-security-default"]; + AssertEqual(true, allSecurity.Includes.Any(id => id.Equals("powershell-security-default", StringComparison.OrdinalIgnoreCase)), + "all-security-default includes powershell-security-default"); + AssertEqual(true, allSecurity.Includes.Any(id => id.Equals("csharp-security-default", StringComparison.OrdinalIgnoreCase)), + "all-security-default includes csharp-security-default"); + + var psSecurity = catalog.Packs["powershell-security-default"]; + foreach (var ruleId in psSecurity.Rules) { + AssertEqual(true, catalog.TryGetRule(ruleId, out _), $"powershell-security-default rule exists: {ruleId}"); + } + } + + private static void TestAnalysisPacksPowerShellDefaultResolves() { + var workspace = ResolveWorkspaceRoot(); + var catalog = IntelligenceX.Analysis.AnalysisCatalogLoader.LoadFromWorkspace(workspace); + + AssertEqual(true, catalog.Packs.ContainsKey("powershell-default"), "pack powershell-default exists"); + AssertEqual(true, catalog.Packs.ContainsKey("powershell-security-default"), "pack powershell-security-default exists"); + + var psDefault = catalog.Packs["powershell-default"]; + AssertEqual(true, psDefault.Includes.Any(id => id.Equals("powershell-security-default", StringComparison.OrdinalIgnoreCase)), + "powershell-default includes powershell-security-default"); + + foreach (var include in psDefault.Includes) { + AssertEqual(true, catalog.Packs.ContainsKey(include), $"powershell-default include exists: {include}"); + } + foreach (var ruleId in psDefault.Rules) { + AssertEqual(true, catalog.TryGetRule(ruleId, out _), $"powershell-default rule exists: {ruleId}"); + } + } +} +#endif diff --git a/IntelligenceX.Tests/Program.cs b/IntelligenceX.Tests/Program.cs index 14dda71b2..8d3beffdc 100644 --- a/IntelligenceX.Tests/Program.cs +++ b/IntelligenceX.Tests/Program.cs @@ -43,6 +43,9 @@ private static int Main() { failed += Run("GitHub secrets reject empty value", TestGitHubSecretsRejectEmptyValue); failed += Run("Release reviewer env token", TestReleaseReviewerEnvToken); #endif + + // Reviewer tests are excluded from NET472 builds (no reviewer references there), and enforced for non-NET472 + // builds via `IntelligenceX.Tests/ReviewerSymbolGuard.cs` + `IntelligenceX.Tests/IntelligenceX.Tests.csproj`. #if INTELLIGENCEX_REVIEWER failed += Run("Cleanup normalize allowed edits", TestCleanupNormalizeAllowedEdits); failed += Run("Cleanup clamp confidence", TestCleanupClampConfidence); @@ -96,7 +99,11 @@ private static int Main() { failed += Run("Analysis catalog validator passes built-in catalog", TestAnalysisCatalogValidatorPassesBuiltInCatalog); failed += Run("Analysis catalog validator detects invalid catalog", TestAnalysisCatalogValidatorDetectsInvalidCatalog); failed += Run("Analysis catalog validator detects missing rule metadata", TestAnalysisCatalogValidatorDetectsMissingRuleMetadata); + failed += Run("Analysis packs: all-security includes PowerShell", TestAnalysisPacksAllSecurityIncludesPowerShell); + failed += Run("Analysis packs: powershell-default resolves", TestAnalysisPacksPowerShellDefaultResolves); failed += Run("Analysis catalog rule overrides apply", TestAnalysisCatalogRuleOverridesApply); + failed += Run("Analysis catalog PowerShell overrides apply", () => TestAnalysisCatalogPowerShellOverridesApply()); + failed += Run("Analysis catalog PowerShell docs links", TestAnalysisCatalogPowerShellDocsLinksMatchLearnPattern); failed += Run("Analysis catalog override invalid type falls back", TestAnalysisCatalogOverrideInvalidTypeFallsBack); failed += Run("Analysis catalog validator rejects dangling override", TestAnalysisCatalogValidatorRejectsDanglingOverride); failed += Run("Analysis hotspots render and state snippet", TestAnalysisHotspotsRenderAndStateSnippet); diff --git a/IntelligenceX.Tests/ReviewerSymbolGuard.cs b/IntelligenceX.Tests/ReviewerSymbolGuard.cs new file mode 100644 index 000000000..1fc0f1848 --- /dev/null +++ b/IntelligenceX.Tests/ReviewerSymbolGuard.cs @@ -0,0 +1,7 @@ +// This project uses a single-binary test harness plus conditional compilation to include reviewer-only tests. +// Make it a build-time error to accidentally ship non-NET472 builds without the reviewer symbol, because +// that would silently omit reviewer test coverage in CI. +#if !NET472 && !INTELLIGENCEX_REVIEWER +#error INTELLIGENCEX_REVIEWER must be defined for non-NET472 builds so reviewer tests are compiled and executed. +#endif + diff --git a/scripts/psscriptanalyzer.version.txt b/scripts/psscriptanalyzer.version.txt new file mode 100644 index 000000000..53cc1a6f9 --- /dev/null +++ b/scripts/psscriptanalyzer.version.txt @@ -0,0 +1 @@ +1.24.0 diff --git a/scripts/sync-pssa-catalog.ps1 b/scripts/sync-pssa-catalog.ps1 new file mode 100644 index 000000000..18f69bee6 --- /dev/null +++ b/scripts/sync-pssa-catalog.ps1 @@ -0,0 +1,600 @@ +param( + [Parameter()][string]$OutDir, + [Parameter()][string]$PSScriptAnalyzerVersion, + [Parameter()][switch]$PruneStale, + [Parameter()][switch]$ForcePrune, + [Parameter()][switch]$AllowNonIntendedOutDir, + [Parameter()][switch]$AllowUntrustedModuleBase +) + +$ErrorActionPreference = 'Stop' + +$runningOnWindows = ($env:OS -eq 'Windows_NT') +$pathComparison = if ($runningOnWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal } + +if ([string]::IsNullOrWhiteSpace($OutDir)) { + $OutDir = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine( + $PSScriptRoot, '..', 'Analysis', 'Catalog', 'rules', 'powershell')) +} + +function Get-NormalizedPath([string]$path) { + if ([string]::IsNullOrWhiteSpace($path)) { return '' } + + # Expand ~ and resolve relative paths without requiring the target to exist. + $unresolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path) + try { + $resolved = (Resolve-Path -LiteralPath $unresolved -ErrorAction Stop).Path + return [System.IO.Path]::GetFullPath($resolved) + } catch { + return [System.IO.Path]::GetFullPath($unresolved) + } +} + +function ConvertTo-JsonEscapedString([string]$value) { + if ($null -eq $value) { return '' } + $sb = New-Object System.Text.StringBuilder + foreach ($ch in $value.ToCharArray()) { + $code = [int][char]$ch + # Switch on codepoints to avoid PowerShell's wildcard matching quirks with `switch` patterns. + switch ($code) { + 0x22 { [void]$sb.Append('\"'); continue } # " + 0x5C { [void]$sb.Append('\\'); continue } # \ + 0x08 { [void]$sb.Append('\b'); continue } + 0x0C { [void]$sb.Append('\f'); continue } + 0x0A { [void]$sb.Append('\n'); continue } + 0x0D { [void]$sb.Append('\r'); continue } + 0x09 { [void]$sb.Append('\t'); continue } + } + if ($code -lt 0x20) { + [void]$sb.Append(("\\u{0:x4}" -f $code)) + continue + } + [void]$sb.Append($ch) + } + return $sb.ToString() +} + +function ConvertTo-DeterministicJson([System.Collections.IDictionary]$obj) { + if ($null -eq $obj) { throw 'JSON object cannot be null' } + + # Keep a stable, intentional order for our known schema keys, and sort any extras for determinism. + $preferredOrder = @( + 'id', + 'language', + 'tool', + 'toolRuleId', + 'title', + 'description', + 'category', + 'defaultSeverity', + 'tags', + 'docs' + ) + $ordered = @() + foreach ($k in $preferredOrder) { + if ($obj.Contains($k)) { $ordered += $k } + } + $extras = @($obj.Keys | Where-Object { $preferredOrder -notcontains $_ } | Sort-Object) + $keys = @($ordered + $extras) + $lines = New-Object System.Collections.Generic.List[string] + [void]$lines.Add('{') + for ($i = 0; $i -lt $keys.Count; $i++) { + $key = [string]$keys[$i] + $value = $obj[$key] + $comma = if ($i -lt ($keys.Count - 1)) { ',' } else { '' } + + if ($value -is [System.Array]) { + [void]$lines.Add((' "{0}": [' -f (ConvertTo-JsonEscapedString $key))) + $arr = @($value) + for ($j = 0; $j -lt $arr.Count; $j++) { + $itemComma = if ($j -lt ($arr.Count - 1)) { ',' } else { '' } + $rawItem = $arr[$j] + if ($null -eq $rawItem) { + throw ("Unsupported JSON array item for key '{0}': null" -f $key) + } + if ($rawItem -isnot [string]) { + throw ("Unsupported JSON array item type for key '{0}': {1}" -f $key, $rawItem.GetType().FullName) + } + $item = [string]$rawItem + [void]$lines.Add((' "{0}"{1}' -f (ConvertTo-JsonEscapedString $item), $itemComma)) + } + [void]$lines.Add((' ]{0}' -f $comma)) + continue + } + + if ($null -eq $value) { + [void]$lines.Add((' "{0}": null{1}' -f (ConvertTo-JsonEscapedString $key), $comma)) + continue + } + + if ($value -isnot [string]) { + throw ("Unsupported JSON value type for key '{0}': {1}" -f $key, $value.GetType().FullName) + } + + [void]$lines.Add((' "{0}": "{1}"{2}' -f (ConvertTo-JsonEscapedString $key), (ConvertTo-JsonEscapedString $value), $comma)) + } + [void]$lines.Add('}') + return ($lines -join "`n") +} + +$versionFromArg = if ([string]::IsNullOrWhiteSpace($PSScriptAnalyzerVersion)) { '' } else { $PSScriptAnalyzerVersion.Trim() } +$pinnedVersionPath = Join-Path -Path $PSScriptRoot -ChildPath 'psscriptanalyzer.version.txt' +$versionFromFile = '' +if ([string]::IsNullOrWhiteSpace($versionFromArg) -and (Test-Path -LiteralPath $pinnedVersionPath)) { + $versionFromFile = (Get-Content -LiteralPath $pinnedVersionPath -Raw).Trim() +} +$resolvedVersionText = if (-not [string]::IsNullOrWhiteSpace($versionFromArg)) { $versionFromArg } else { $versionFromFile } +if ([string]::IsNullOrWhiteSpace($resolvedVersionText)) { + throw ("PSScriptAnalyzer version is not pinned. Specify -PSScriptAnalyzerVersion, or create '{0}' containing the intended version (e.g. 1.24.0)." -f $pinnedVersionPath) +} + +try { + $resolvedVersion = [version]$resolvedVersionText +} catch { + throw ("Invalid PSScriptAnalyzer version '{0}'. Expected a semantic version like 1.24.0." -f $resolvedVersionText) +} + +$module = Get-Module -ListAvailable -Name PSScriptAnalyzer | + Where-Object { $_.Version -eq $resolvedVersion } | + Sort-Object Path, ModuleBase | + Select-Object -First 1 +if (-not $module) { + $installed = Get-Module -ListAvailable -Name PSScriptAnalyzer | + Sort-Object Version -Descending | + Select-Object -ExpandProperty Version -Unique | + ForEach-Object { $_.ToString() } + $installedMsg = if ($installed -and $installed.Count -gt 0) { ($installed -join ', ') } else { 'none' } + throw ("PSScriptAnalyzer {0} module not found. Installed versions: {1}. Install with: Install-Module PSScriptAnalyzer -RequiredVersion {0} -Scope CurrentUser" -f $resolvedVersion, $installedMsg) +} +Write-Output ("Using PSScriptAnalyzer {0} from '{1}'." -f $module.Version, $module.ModuleBase) + +# Avoid importing a module that is (accidentally or maliciously) located under the repo workspace. +$workspaceRoot = Get-NormalizedPath (Join-Path -Path $PSScriptRoot -ChildPath '..') +if ($module.ModuleBase) { + $root = Get-NormalizedPath $workspaceRoot + $base = Get-NormalizedPath $module.ModuleBase + $rootTrim = $root.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + $rootPrefix = $rootTrim + [System.IO.Path]::DirectorySeparatorChar + + $isInWorkspace = + $base.Equals($rootTrim, $pathComparison) -or + $base.StartsWith($rootPrefix, $pathComparison) + if ($isInWorkspace) { + throw ("Refusing to import PSScriptAnalyzer from workspace path: {0}" -f $base) + } +} + +function Test-IsUnderPath([string]$path, [string]$root) { + if ([string]::IsNullOrWhiteSpace($path) -or [string]::IsNullOrWhiteSpace($root)) { return $false } + try { + $fullPath = Get-NormalizedPath $path + $fullRoot = Get-NormalizedPath $root + } catch { + return $false + } + $trimRoot = $fullRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + $prefix = $trimRoot + [System.IO.Path]::DirectorySeparatorChar + return $fullPath.Equals($trimRoot, $pathComparison) -or + $fullPath.StartsWith($prefix, $pathComparison) +} + +function Test-IsTrustedModuleBase([string]$moduleBase) { + if ([string]::IsNullOrWhiteSpace($moduleBase)) { return $false } + $trustedRoots = @() + + # PSHOME modules are considered trusted. + if ($PSHOME) { $trustedRoots += (Join-Path -Path $PSHOME -ChildPath 'Modules') } + + # Common system-wide module locations. + if ($runningOnWindows) { + if ($env:ProgramFiles) { + $trustedRoots += (Join-Path -Path $env:ProgramFiles -ChildPath (Join-Path -Path 'WindowsPowerShell' -ChildPath 'Modules')) + $trustedRoots += (Join-Path -Path $env:ProgramFiles -ChildPath (Join-Path -Path 'PowerShell' -ChildPath 'Modules')) + } + } else { + $trustedRoots += '/usr/local/share/powershell/Modules' + $trustedRoots += '/usr/share/powershell/Modules' + } + + foreach ($root in $trustedRoots) { + if (-not (Test-Path -LiteralPath $root)) { continue } + if (Test-IsUnderPath $moduleBase $root) { return $true } + } + return $false +} + +function Test-IsTrustedAuthenticode([string]$path) { + if ([string]::IsNullOrWhiteSpace($path)) { return $false } + if (-not $runningOnWindows) { return $false } + try { + $sig = Get-AuthenticodeSignature -FilePath $path + if ($sig -and $sig.Status -eq 'Valid' -and $sig.SignerCertificate) { + return $true + } + } catch { + # Ignore Authenticode lookup failures (not all platforms support it consistently). + Write-Verbose ("Authenticode signature check failed for '{0}': {1}" -f $path, $_.Exception.Message) + } + return $false +} + +# Refuse to import PSScriptAnalyzer from arbitrary PSModulePath locations. Prefer PSHOME/system paths. +$trustedBase = $false +if ($module.ModuleBase) { + $trustedBase = Test-IsTrustedModuleBase $module.ModuleBase +} +$trustedSig = $false +if ($runningOnWindows) { + $verifyPaths = @() + if ($module.Path) { $verifyPaths += $module.Path } + if ($module.ModuleBase -and $module.RootModule) { + $rootCandidate = Join-Path -Path $module.ModuleBase -ChildPath $module.RootModule + if (Test-Path -LiteralPath $rootCandidate) { $verifyPaths += $rootCandidate } + } + foreach ($verifyPath in $verifyPaths) { + if (Test-IsTrustedAuthenticode $verifyPath) { + $trustedSig = $true + break + } + } +} +if (-not ($trustedBase -or $trustedSig)) { + if ($AllowUntrustedModuleBase) { + Write-Warning ("Importing PSScriptAnalyzer from an untrusted location because -AllowUntrustedModuleBase was set. ModuleBase='{0}', Path='{1}'." -f $module.ModuleBase, $module.Path) + } else { + $baseMsg = $module.ModuleBase + $pathMsg = $module.Path + throw ("Refusing to import PSScriptAnalyzer from an untrusted location. ModuleBase='{0}', Path='{1}'. Install with -Scope AllUsers, or pass -AllowUntrustedModuleBase to proceed anyway." -f $baseMsg, $pathMsg) + } +} + +# Import by explicit manifest/module path after trust checks to avoid module shadowing via PSModulePath. +# (Importing by name/version can still resolve to a different module first, executing attacker code on import.) +if (-not $module.Path) { + throw ("PSScriptAnalyzer module path is missing for version {0}. Cannot import safely." -f $module.Version) +} +try { + # If the module is already loaded, remove it so we import the inspected instance. + Remove-Module -Name PSScriptAnalyzer -Force -ErrorAction SilentlyContinue +} catch { + # Ignore removal errors; Import-Module by explicit path will still fail if the session can't load it. + Write-Verbose ("Remove-Module PSScriptAnalyzer failed (ignored): {0}" -f $_.Exception.Message) +} +Import-Module -Name $module.Path -ErrorAction Stop +$expectedPathNorm = Get-NormalizedPath $module.Path +$imported = Get-Module -Name PSScriptAnalyzer -ErrorAction Stop | + Where-Object { $_.Version -eq $module.Version -and $_.Path -and (Get-NormalizedPath $_.Path).Equals($expectedPathNorm, $pathComparison) } | + Select-Object -First 1 +if (-not $imported) { + throw ("Failed to import PSScriptAnalyzer {0}." -f $module.Version) +} +if ($module.ModuleBase -and $imported.ModuleBase) { + $expectedBase = Get-NormalizedPath $module.ModuleBase + $actualBase = Get-NormalizedPath $imported.ModuleBase + if (-not $actualBase.Equals($expectedBase, $pathComparison)) { + throw ("Imported PSScriptAnalyzer ModuleBase does not match expected ModuleBase. Expected='{0}', actual='{1}'." -f $expectedBase, $actualBase) + } +} +if ($module.Path -and $imported.Path) { + $expectedPath = Get-NormalizedPath $module.Path + $actualPath = Get-NormalizedPath $imported.Path + if (-not $actualPath.Equals($expectedPath, $pathComparison)) { + throw ("Imported PSScriptAnalyzer Path does not match expected Path. Expected='{0}', actual='{1}'." -f $expectedPath, $actualPath) + } +} + +New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + +$intendedOutDir = [System.IO.Path]::GetFullPath((Join-Path -Path $workspaceRoot -ChildPath (Join-Path -Path 'Analysis' -ChildPath (Join-Path -Path 'Catalog' -ChildPath (Join-Path -Path 'rules' -ChildPath 'powershell'))))) +try { + # Resolve from the existing directory (created above) to keep pruning decisions predictable. + $resolvedOutDir = [System.IO.Path]::GetFullPath((Resolve-Path -LiteralPath $OutDir -ErrorAction Stop).Path) +} catch { + throw ("Failed to resolve OutDir '{0}': {1}" -f $OutDir, $_.Exception.Message) +} +$resolvedIntendedOutDir = $intendedOutDir +if (Test-Path -LiteralPath $intendedOutDir) { + try { + $resolvedIntendedOutDir = Get-NormalizedPath $intendedOutDir + } catch { + throw ("Failed to resolve intended OutDir '{0}': {1}" -f $intendedOutDir, $_.Exception.Message) + } +} + +# Even with -ForcePrune, never allow pruning outside the repo workspace. +try { + $workspaceFull = [System.IO.Path]::GetFullPath((Resolve-Path -LiteralPath $workspaceRoot -ErrorAction Stop).Path) +} catch { + throw ("Failed to resolve workspace root '{0}': {1}" -f $workspaceRoot, $_.Exception.Message) +} +$workspaceTrim = $workspaceFull.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) +$workspacePrefix = $workspaceTrim + [System.IO.Path]::DirectorySeparatorChar +$isUnderWorkspace = + $resolvedOutDir.Equals($workspaceTrim, $pathComparison) -or + $resolvedOutDir.StartsWith($workspacePrefix, $pathComparison) +if ($PruneStale -and (-not $isUnderWorkspace)) { + throw ("Refusing to prune outside workspace. OutDir='{0}', workspace='{1}'." -f $resolvedOutDir, $workspaceTrim) +} + +if ($PruneStale -and (-not $resolvedOutDir.Equals($resolvedIntendedOutDir, $pathComparison))) { + if (-not $ForcePrune) { + throw ("Refusing to prune outside intended catalog directory. OutDir='{0}', intended='{1}'. Pass -ForcePrune to prune." -f $resolvedOutDir, $resolvedIntendedOutDir) + } + if (-not $AllowNonIntendedOutDir) { + throw ("Refusing to prune outside intended catalog directory. OutDir='{0}', intended='{1}'. Pass -AllowNonIntendedOutDir to explicitly allow pruning a non-standard directory." -f $resolvedOutDir, $resolvedIntendedOutDir) + } +} + +$securityRules = @( + 'PSAvoidUsingAllowUnencryptedAuthentication', + 'PSAvoidUsingBrokenHashAlgorithms', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + 'PSAvoidUsingInvokeExpression', + 'PSAvoidUsingPlainTextForPassword', + 'PSAvoidUsingUsernameAndPasswordParams', + 'PSUsePSCredentialType' +) | ForEach-Object { $_.ToLowerInvariant() } + +function Get-Category([string]$ruleName) { + if ($securityRules -contains $ruleName.ToLowerInvariant()) { return 'Security' } + if ($ruleName.StartsWith('PSPossibleIncorrect', [System.StringComparison]::OrdinalIgnoreCase)) { return 'Reliability' } + return 'BestPractices' +} + +function Get-DefaultSeverity([string]$severity) { + switch ($severity) { + 'Error' { return 'error' } + 'Information' { return 'info' } + default { return 'warning' } + } +} + +function Compress-Whitespace([string]$text) { + if ([string]::IsNullOrWhiteSpace($text)) { return '' } + ($text -replace '[\r\n]+', ' ' -replace '\s+', ' ').Trim() +} + +function Format-MetadataText([string]$text) { + if ([string]::IsNullOrWhiteSpace($text)) { return '' } + $fixed = $text + # Upstream typos we don't want to publish as-is. + $fixed = $fixed -replace '\bwhitepsace\b', 'whitespace' + $fixed = $fixed -replace '\bautomtic\b', 'automatic' + $fixed = $fixed -replace '\breadonly\b', 'read-only' + $fixed = $fixed -replace '\bPrameter\b', 'Parameter' + $fixed = $fixed -replace '\bequaltiy\b', 'equality' + $fixed = $fixed -replace '\bcomaprision\b', 'comparison' + $fixed = $fixed -replace '\bfunctiosn\b', 'functions' + $fixed = $fixed -replace '\bindenation\b', 'indentation' + $fixed = $fixed -replace '\bassigment\b', 'assignment' + # Clean up accidental double periods that occasionally show up upstream (e.g. "ignored.. To"). + $fixed = $fixed -replace '\.\.\s+', '. ' + $fixed = $fixed -replace '\.\.$', '.' + $fixed +} + +function Get-RuleTitleFromRuleName([string]$ruleName) { + if ([string]::IsNullOrWhiteSpace($ruleName)) { return '' } + $name = $ruleName.Trim() + if ($name.StartsWith('PS', [System.StringComparison]::OrdinalIgnoreCase)) { + $name = $name.Substring(2) + } + # Insert spaces for PascalCase while keeping acronyms (UTF8, DSC, WMI) reasonably intact. + $name = $name -creplace '([a-z0-9])([A-Z])', '$1 $2' + $name = $name -creplace '([A-Z])([A-Z][a-z])', '$1 $2' + $name.Trim() +} + +function Write-FileUtf8NoBomLf([string]$path, [string]$content) { + # Avoid BOM differences across PowerShell versions and normalize newlines for stable diffs. + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + $normalized = ($content -replace "`r`n", "`n" -replace "`r", "`n") + if (-not $normalized.EndsWith("`n")) { $normalized += "`n" } + [System.IO.File]::WriteAllText($path, $normalized, $utf8NoBom) +} + +$rules = @() +try { + $rules = @(Get-ScriptAnalyzerRule | Sort-Object RuleName) +} catch { + throw ("Get-ScriptAnalyzerRule failed: {0}" -f $_.Exception.Message) +} +if (-not $rules -or $rules.Count -eq 0) { + throw "Get-ScriptAnalyzerRule returned 0 rules. Ensure PSScriptAnalyzer imported correctly and is functional in this session." +} +$ruleIds = @($rules | ForEach-Object { [string]$_.RuleName } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) +$ruleIdSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) +foreach ($id in $ruleIds) { [void]$ruleIdSet.Add($id) } +if ($ruleIdSet.Count -ne $ruleIds.Count) { + $counts = @{} + foreach ($id in $ruleIds) { + $key = $id.ToLowerInvariant() + if ($counts.ContainsKey($key)) { $counts[$key]++ } else { $counts[$key] = 1 } + } + $duplicates = @($counts.GetEnumerator() | Where-Object { $_.Value -gt 1 } | ForEach-Object { $_.Key } | Sort-Object) + throw ("Duplicate PSScriptAnalyzer rule names detected ({0}): {1}" -f $duplicates.Count, ($duplicates -join ', ')) +} + +function Get-LearnDocsUrl([string]$ruleName) { + if ([string]::IsNullOrWhiteSpace($ruleName)) { return $null } + $name = $ruleName.Trim() + if ($name.StartsWith('PS', [System.StringComparison]::OrdinalIgnoreCase)) { + # Learn slugs omit the leading PS prefix (e.g. PSAvoidLongLines -> avoidlonglines). The PS-prefixed + # variant 404s at time of writing. + $name = $name.Substring(2) + } + $slug = $name.ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($slug)) { return $null } + return ('https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/{0}' -f $slug) +} + +$existingDocsByRule = @{} +try { + if (Test-Path -LiteralPath $OutDir) { + foreach ($file in [System.IO.Directory]::EnumerateFiles($OutDir, '*.json')) { + $id = [System.IO.Path]::GetFileNameWithoutExtension($file) + if ([string]::IsNullOrWhiteSpace($id)) { continue } + try { + $existing = Get-Content -LiteralPath $file -Raw -ErrorAction Stop | ConvertFrom-Json + $existingDocs = [string]$existing.docs + if (-not [string]::IsNullOrWhiteSpace($existingDocs)) { + $existingDocsByRule[$id] = $existingDocs.Trim() + } + } catch { + # Ignore read/parse errors; we can still generate stable docs from Learn. + continue + } + } + } +} catch { + throw ("Failed to preload existing docs from OutDir '{0}': {1}" -f $OutDir, $_.Exception.Message) +} + +foreach ($rule in $rules) { + $ruleName = [string]$rule.RuleName + if ([string]::IsNullOrWhiteSpace($ruleName)) { continue } + if ($ruleName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) { + throw ("RuleName contains invalid filename characters: '{0}'." -f $ruleName) + } + + $title = Compress-Whitespace ([string]$rule.CommonName) + if ([string]::IsNullOrWhiteSpace($title)) { $title = Get-RuleTitleFromRuleName $ruleName } + if ([string]::IsNullOrWhiteSpace($title)) { $title = $ruleName } + + $description = Compress-Whitespace ([string]$rule.Description) + if ([string]::IsNullOrWhiteSpace($description)) { $description = "PSScriptAnalyzer rule '$ruleName'. See docs for details." } + + $title = Format-MetadataText $title + $description = Format-MetadataText $description + + switch ($ruleName) { + 'PSAvoidAssignmentToAutomaticVariable' { + $title = 'Changing automatic variables might have undesired side effects' + $description = 'Automatic variables are built into PowerShell and are read-only. Avoid assigning to them.' + } + 'PSAlignAssignmentStatement' { + $title = 'Align Assignment Statements' + $description = 'Line up assignment statements so that the assignment operators are aligned.' + } + 'PSPossibleIncorrectComparisonWithNull' { + $title = 'Possible Incorrect Comparison With Null' + $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 whether 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.' + } + 'PSUseConsistentWhitespace' { + $title = 'Use Consistent Whitespace' + $description = "Check for whitespace between keyword and open paren/curly, around assignment operator ('='), around arithmetic operators and after separators (',' and ';')." + } + } + + # Minor grammar cleanup + $description = $description -replace '\boperator are\b', 'operators are' + if (-not [string]::IsNullOrWhiteSpace($description) -and $description -notmatch '[\.\!\?]$') { + $description = $description + '.' + } + + $path = Join-Path $OutDir ($ruleName + '.json') + + # Prefer a stable Learn URL. Only preserve existing docs if it matches the expected Learn URL exactly + # (no query/fragment and correct slug), to avoid carrying forward bad/unstable URLs forever. + $docs = Get-LearnDocsUrl $ruleName + if ([string]::IsNullOrWhiteSpace($docs)) { + throw ("Failed to compute Learn docs URL for rule '{0}'." -f $ruleName) + } + if ($existingDocsByRule.ContainsKey($ruleName)) { + $existingDocs = [string]$existingDocsByRule[$ruleName] + if (-not [string]::IsNullOrWhiteSpace($existingDocs)) { + try { + $existingUri = [System.Uri]::new($existingDocs) + $expectedUri = [System.Uri]::new($docs) + if ($existingUri.Scheme -eq 'https' -and + $existingUri.Host -eq 'learn.microsoft.com' -and + [string]::IsNullOrEmpty($existingUri.Query) -and + [string]::IsNullOrEmpty($existingUri.Fragment) -and + $existingUri.AbsolutePath -eq $expectedUri.AbsolutePath) { + $docs = $existingDocs + } + } catch { + # Ignore invalid existing docs; fall back to Learn. + $existingDocs = $null + } + } + } + + $category = Get-Category $ruleName + $defaultSeverity = Get-DefaultSeverity ([string]$rule.Severity) + + $tags = @('powershell', 'psscriptanalyzer') + if ($category -eq 'Security') { $tags += 'security' } + if ($category -eq 'Reliability') { $tags += 'reliability' } + if ($category -eq 'BestPractices') { $tags += 'best-practices' } + + $obj = [ordered]@{ + id = $ruleName + language = 'powershell' + tool = 'PSScriptAnalyzer' + toolRuleId = $ruleName + title = $title + description = $description + category = $category + defaultSeverity = $defaultSeverity + tags = $tags + docs = $docs + } + + $json = ConvertTo-DeterministicJson $obj + Write-FileUtf8NoBomLf $path $json +} + +# Delete stale rule files so the repo doesn't accumulate orphaned rules over time. +$existingRuleFiles = @() +try { + $existingRuleFiles = @(Get-ChildItem -LiteralPath $OutDir -Filter '*.json' -File -ErrorAction Stop) +} catch { + throw ("Failed to enumerate existing rule JSON files in OutDir '{0}': {1}" -f $OutDir, $_.Exception.Message) +} +$staleRuleFiles = @() +foreach ($file in $existingRuleFiles) { + $id = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) + if ($id -and -not $ruleIdSet.Contains($id)) { + $staleRuleFiles += $file + } +} + +# Also delete stale overrides for rules that no longer exist. +$staleOverrideFiles = @() +if ($resolvedOutDir.Equals($resolvedIntendedOutDir, $pathComparison)) { + $rulesRoot = Split-Path -Parent $OutDir + $catalogRoot = Split-Path -Parent $rulesRoot + $overridesDir = Join-Path -Path $catalogRoot -ChildPath (Join-Path -Path 'overrides' -ChildPath 'powershell') + if (Test-Path -LiteralPath $overridesDir) { + try { + foreach ($file in @(Get-ChildItem -LiteralPath $overridesDir -Filter '*.json' -File -ErrorAction Stop)) { + $id = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) + if ($id -and -not $ruleIdSet.Contains($id)) { $staleOverrideFiles += $file } + } + } catch { + throw ("Failed to enumerate override JSON files in '{0}': {1}" -f $overridesDir, $_.Exception.Message) + } + } +} elseif ($PruneStale) { + Write-Warning ("Skipping overrides pruning because OutDir does not match intended catalog directory. OutDir='{0}', intended='{1}'." -f $resolvedOutDir, $resolvedIntendedOutDir) +} + +$deleted = 0 +if (($staleRuleFiles.Count -gt 0) -or ($staleOverrideFiles.Count -gt 0)) { + $totalStale = $staleRuleFiles.Count + $staleOverrideFiles.Count + if (-not $PruneStale) { + Write-Warning ("Found {0} stale file(s). Re-run with -PruneStale to delete them." -f $totalStale) + } else { + foreach ($file in $staleRuleFiles) { + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop + $deleted++ + } + foreach ($file in $staleOverrideFiles) { + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop + $deleted++ + } + } +} + +Write-Output ("Wrote {0} rule file(s) to {1} (deleted {2} stale file(s))" -f $rules.Count, $OutDir, $deleted)