powershellparametersabstract-syntax-tree

How to resolve command name and parameters from a CommandAst


With writing custom PSSA rules and just parsing PowerShell scripts, I find myself often in the same use case where I would like to resolve command name (which is not that difficult) and the parameters of a CommandAst.
For my current particular case, I would like the write a rule to Avoid New-Object Cmdlet. To prevent any false positives, I would like to exclude any command with New-Object -ComObject parameters, and to write an automated -Fix I would like to retrieve parameters along with the -TypeName parameter. The "problem" is that the parameters could be provided several ways: named, unnamed (by position) or elastic (e.g. -Type) or an alias as -Args.
I presume that the PowerShell engine somehow needs to deal with the same command line resolving and I wonder whether this could be invoked from PowerShell itself.

In other words, I have the following command line:

New-Object -TypeName System.Version -ArgumentList "1.2.3.4"

which I can parse with the Abstract Syntax Tree (AST) class.
To resolve the command name (regardsless I might use an alias), I might do this:

$Ast = [System.Management.Automation.Language.Parser]::ParseInput(
    { New-Object -TypeName System.Version -ArgumentList "1.2.3.4" },
    [ref]$null, [ref]$null
)
$CommandAst = $Ast.Endblock.Statements.PipelineElements
$CommandAst.GetCommandName()
New-Object

But how do I (easily) resolve the parameters (regardless they are provided named, unnamed, elastic or via an alias)?


Solution

  • You can use StaticParameterBinder.BindCommand, this of course works as long as the command exist / can be resolved:

    using namespace System.Management.Automation.Language
    
    $command = { New-Object System.Version '1.2.3.4' }
    $commandAst = $command.Ast.Find({ $args[0] -is [CommandAst] }, $false)
    $bindingResult = [StaticParameterBinder]::BindCommand($commandAst)
    
    foreach ($result in $bindingResult.BoundParameters.GetEnumerator()) {
        [pscustomobject]@{
            Parameter = $result.Key
            Argument  = $result.Value.Value.SafeGetValue()
        }
    }
    

    There is a caveat with .SafeGetValue(), if the dynamic expression cannot be resolved, you will get an error, so for example if your command was:

    $command = {
        $foo = '1.2.3.4'
        New-Object System.Version $foo
    }
    

    Then, it would fail, if you want to resolve the value of $foo, in PowerShell 7+ you can use .SafeGetvalue($true), however this overload doesn't exist in in older versions, there you will require a more manual approach, an example here.

    In this case the output you'd get from the above would be:

    Parameter    Argument
    ---------    --------
    TypeName     System.Version
    ArgumentList 1.2.3.4
    

    If however you want to see the argument as-is, without resolving it, then you can use .ToString() instead of .SafeGetValue(), in which case the output would be:

    Parameter    Argument
    ---------    --------
    TypeName     System.Version
    ArgumentList $foo