GoodEnoughRules.psm1


function Get-CommandParameters {
    param (
        [System.Management.Automation.Language.CommandAst]
        $Command
    )

    $commandElements = $CommandAst.CommandElements
    #region Gather Parameters
    # Create a hash to hold the parameter name as the key, and the value
    $parameterHash = @{}
    for($i=1; $i -lt $commandElements.Count; $i++){
        # Switch on type
        switch ($commandElements[$i].GetType().Name){
            'CommandParameterAst' {
                $parameterName = $commandElements[$i].ParameterName
            }
            default {
                # Grab the string from the extent
                $value = $commandElements[$i].SafeGetValue()
                $parameterHash[$parameterName] = $value
            }
        }
    }
    return $parameterHash
}
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-CommandParameters -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
                    Severity = [Microsoft.Windows.PowerShell.ScriptAnalyzer.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!