GoodEnoughRules.psm1
|
function Get-CommandParameter { [CmdletBinding()] [OutputType([hashtable])] param ( [System.Management.Automation.Language.CommandAst] $Command ) $commandElements = $Command.CommandElements Write-Verbose "Processing command: $($Command.GetCommandName())" Write-Verbose "Total command elements: $($commandElements.Count - 1)" #region Gather Parameters # Create a hash to hold the parameter name as the key, and the value $parameterHash = @{} # Track positional parameter index $positionalIndex = 0 # Start at index 1 to skip the command name for ($i = 1; $i -lt $commandElements.Count; $i++) { Write-Debug $commandElements[$i] # Switch on type switch ($commandElements[$i].GetType().Name) { 'ParameterAst' { $parameterName = $commandElements[$i].ParameterName # Next element is the value continue } 'CommandParameterAst' { $parameterName = $commandElements[$i].ParameterName # Initialize to $true for switch parameters $parameterHash[$parameterName] = $true continue } 'StringConstantExpressionAst' { $value = $commandElements[$i].Value # Check if a parameter name was set if (-not $parameterName) { Write-Verbose "Positional parameter or argument detected: $value" $parameterHash["PositionalParameter$positionalIndex"] = $value $positionalIndex++ continue } $parameterHash[$parameterName] = $value continue } default { Write-Verbose "Unhandled command element type: $($commandElements[$i].GetType().Name)" # Grab the string from the extent $value = $commandElements[$i].SafeGetValue() $parameterHash[$parameterName] = $value } } } return $parameterHash } function Measure-BasicWebRequestProperty { <# .SYNOPSIS Rule to detect if Invoke-WebRequest is used with UseBasicParsing and incompatible properties. .DESCRIPTION This rule detects if Invoke-WebRequest (or its aliases) is used with the UseBasicParsing parameter and then attempts to access properties that are incompatible with UseBasicParsing. This includes properties like 'Forms', 'ParsedHtml', 'Scripts', and 'AllElements'. This checks for both direct member access after the command as well as variable assignments. .PARAMETER ScriptBlockAst The scriptblock AST to check. .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .EXAMPLE Measure-BasicWebRequestProperty -ScriptBlockAst $ScriptBlockAst This will check if the given ScriptBlockAst contains any Invoke-WebRequest commands with UseBasicParsing that access incompatible properties. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) begin { # We need to find any assignments or uses of Invoke-WebRequest (or its aliases) # to check if they attempt to use incompatible properties with UseBasicParsing. # Examples to find: # $bar = (iwr -Uri 'https://example.com' -UseBasicParsing).Forms $iwrMemberRead = { param($Ast) $Ast -is [System.Management.Automation.Language.CommandAst] -and # IWR Command $Ast.GetCommandName() -match '(Invoke-WebRequest|iwr|curl)' -and # With UseBasicParsing $Ast.CommandElements.ParameterName -contains 'UseBasicParsing' -and # That is being read as a member expression $Ast.Parent.Parent -is [System.Management.Automation.Language.ParenExpressionAst] -and $Ast.Parent.Parent.Parent -is [System.Management.Automation.Language.MemberExpressionAst] -and # The member being accessed is a string constant $Ast.Parent.Parent.Parent.Member -is [System.Management.Automation.Language.StringConstantExpressionAst] -and # The member is one of the incompatible properties $incompatibleProperties -contains $Ast.Parent.Parent.Parent.Member } # Predicate to find assignments involving Invoke-WebRequest with UseBasicParsing # $foo = Invoke-WebRequest -Uri 'https://example.com' -UseBasicParsing $varAssignPredicate = { param($Ast) $Ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and $Ast.Right -is [System.Management.Automation.Language.PipelineAst] -and $Ast.Right.PipelineElements.Count -eq 1 -and $Ast.Right.PipelineElements[0] -is [System.Management.Automation.Language.CommandAst] -and $Ast.Right.PipelineElements[0].GetCommandName() -match '(Invoke-WebRequest|iwr|curl)' -and $Ast.Right.PipelineElements[0].CommandElements.ParameterName -contains 'UseBasicParsing' } $incompatibleProperties = @( 'AllElements', 'Forms', 'ParsedHtml', 'Scripts' ) } process { # Find all member expression ASTs that match our criteria [System.Management.Automation.Language.Ast[]]$memberReads = $ScriptBlockAst.FindAll($iwrMemberRead, $true) foreach ($memberRead in $memberReads) { # ParenExpression would be the whole line: (iwr -Uri ... -UseBasicParsing).Foo $parenExpression = $memberRead.Parent.Parent $propertyName = $memberRead.Parent.Parent.Parent.Member.Value [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ Message = "Invoke-WebRequest cannot use the '$propertyName' parameter when 'UseBasicParsing' is specified." Extent = $parenExpression.Extent Severity = 'Error' } } # Find all assignment ASTs that match our criteria [System.Management.Automation.Language.Ast[]]$assignments = $ScriptBlockAst.FindAll($varAssignPredicate, $true) # Now use that to search for var reads of the assigned variable foreach ($assignment in $assignments) { $variableName = $assignment.Left.VariablePath.UserPath $lineAfter = $assignment.Extent.EndLineNumber Write-Verbose "Checking variable '$variableName' for incompatible property usage after line $lineAfter" # Find all reads of that variable #region Dynamically Build the AST Search Predicate $varReadPredicateScript = @() $varReadPredicateScript += 'param($Ast)' $varReadPredicateScript += '$Ast -is [System.Management.Automation.Language.VariableExpressionAst] -and' $varReadPredicateScript += '$Ast.VariablePath.UserPath -eq "' + $variableName + '" -and' $varReadPredicateScript += '$Ast.Extent.StartLineNumber -gt ' + $lineAfter $varReadPredicate = [scriptblock]::Create($($varReadPredicateScript -join "`n")) [System.Management.Automation.Language.Ast[]]$varReads = $ScriptBlockAst.FindAll($varReadPredicate, $true) foreach ($varRead in $varReads) { Write-Verbose "Found read of variable '$variableName' at line $($varRead.Extent.StartLineNumber)" if ($varRead.Parent -is [System.Management.Automation.Language.MemberExpressionAst]) { $propertyName = $varRead.Parent.Member.Value if ($incompatibleProperties -contains $propertyName) { [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ Message = "Invoke-WebRequest cannot use the '$propertyName' parameter when 'UseBasicParsing' is specified." RuleName = $PSCmdlet.MyInvocation.InvocationName Extent = $varRead.Parent.Extent Severity = 'Error' } } } } } } } function Measure-InvokeWebRequestWithoutBasic { <# .SYNOPSIS Rule to detect if Invoke-WebRequest is used without UseBasicParsing. .DESCRIPTION This rule detects if Invoke-WebRequest (or its aliases) is used without the UseBasicParsing parameter. .PARAMETER ScriptBlockAst The scriptblock AST to check. .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .EXAMPLE Measure-InvokeWebRequestWithoutBasic -ScriptBlockAst $ScriptBlockAst This will check if the given ScriptBlockAst contains any Invoke-WebRequest commands without the UseBasicParsing parameter. #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) begin { $predicate = { param($Ast) $Ast -is [System.Management.Automation.Language.CommandAst] -and $Ast.GetCommandName() -imatch '(Invoke-WebRequest|iwr|curl)$' } } process { [System.Management.Automation.Language.Ast[]]$commands = $ScriptBlockAst.FindAll($predicate, $true) $commands | ForEach-Object { Write-Verbose "Analyzing command: $($_.GetCommandName())" $command = $_ $parameterHash = Get-CommandParameter -Command $command if (-not $parameterHash.ContainsKey('UseBasicParsing')) { [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ Message = 'Invoke-WebRequest should be used with the UseBasicParsing parameter.' Extent = $command.Extent RuleName = $PSCmdlet.MyInvocation.InvocationName Severity = 'Error' } } } } } function Measure-SecureStringWithKey { <# .SYNOPSIS Rule to detect if ConvertFrom-SecureString is used without a Key. .DESCRIPTION This rule detects if ConvertFrom-SecureString is used without a Key which means the secret is user and machine bound. .EXAMPLE Measure-SecureStringWithKey -ScriptBlockAst $ScriptBlockAst This will check if the given ScriptBlockAst contains any ConvertFrom-SecureString commands without a Key parameter. .PARAMETER ScriptBlockAst The scriptblock AST to check. .INPUTS [System.Management.Automation.Language.ScriptBlockAst] .OUTPUTS [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES None #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst] $ScriptBlockAst ) begin { $predicate = { param($Ast) $Ast -is [System.Management.Automation.Language.CommandAst] -and $Ast.GetCommandName() -eq 'ConvertFrom-SecureString' } } process { [System.Management.Automation.Language.Ast[]]$commands = $ScriptBlockAst.FindAll($predicate, $true) $commands | ForEach-Object { $command = $_ $parameterHash = Get-CommandParameter -Command $command if (-not $parameterHash.ContainsKey('Key')) { [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ Message = 'ConvertFrom-SecureString should be used with a Key.' Extent = $command.Extent RuleName = $PSCmdlet.MyInvocation.InvocationName Severity = 'Error' } } } } } function Measure-TODOComment { <# .SYNOPSIS Rule to detect if TODO style comments are present. .DESCRIPTION This rule detects if TODO style comments are present in the given ScriptBlockAst. .EXAMPLE Measure-TODOComment -Token $Token This will check if the given Token contains any TODO comments. .PARAMETER Token The token to check for TODO comments. .INPUTS [System.Management.Automation.Language.Token] .OUTPUTS [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] .NOTES None #> [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.Token] $Token ) Begin { $toDoIndicators = @( 'TODO', 'FIXME', 'BUG', 'MARK', '\[.\]' ) -join '|' $regExOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase #, IgnorePatternWhitespace, Multiline" # TODO: Add more comments to make it easier to understand the regular expression. # so meta hehe $regExPattern = "((\/\/|#|<!--|;|\*|^)((\s+(!|\?|\*|\-))?(\s+\[ \])?|(\s+(!|\?|\*)\s+\[.\])?)\s*($toDoIndicators)\s*\:?)" $regEx = [regex]::new($regExPattern, $regExOptions) } Process { if (-not $Token.Type -ne 'Comment') { return } #region Finds ASTs that match the predicates. foreach ($i in $Token.Extent.Text) { try { $matches = $regEx.Matches($i) } catch { $PSCmdlet.ThrowTerminatingError($PSItem) } if ($matches.Count -eq 0) { continue } $matches | ForEach-Object { [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ 'Message' = "TODO comment found. Please consider removing it or tracking with issue." 'Extent' = $Token.Extent 'RuleName' = $PSCmdlet.MyInvocation.InvocationName 'Severity' = 'Information' } } } #endregion } } # Don't need to dot load because we are compiling! |