PowerShell | How to cross-reference parameters between 2 argument completers?

153 Views Asked by At

My PowerShell module has 2 argument completers. The 2 parameters with argument completers are related to each other in a way that by calculating the value of one of them, we can get the value of the other one.

I want to use this relation to make sure when both of those parameters are being used, they only suggest unique values.

Remove-WDACConfig -UnsignedOrSupplemental -PolicyIDs a244370e-44c9-4c06-b551-f6016e563076,d3645984-47a0-4c8e-be75-1c06840e13e6,38734d8a-4bc4-4dd3-b23f-57f536814426,e63679a6-ae84-4d27-b842-258217562941 -PolicyNames 'Microsoft Windows Driver Policy - Enforced','Supplemental Policy 1 - 05-16-2023','Supplemental Policy 2 - 05-16-2023','Allow Microsoft Plus Block Rules - 05-16-2023'

As you can see in the command above, there are 4 policies deployed. I selected 4 of them by their IDs and then selected the same 4 with their names. Running that command throws an error for the next 4 since PowerShell can't find them anymore when they are already removed by IDs.

I want to change the argument completers so that when I specify say 2 of them by name, the ID of those 2 shouldn't appear when argument completing the IDs.

The values are Code Integrity policy IDs and names. This is related to a previous question.

I did try to modify it but couldn't get it to work exactly the way I want.

[ArgumentCompleter({
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
        $candidates = [PolicyIDz]::new().GetValidValues() | ForEach-Object { if ($_ -notin $fakeBoundParameters) { $_ } }
        $existing = $commandAst.FindAll({ 
                $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
            }, 
            $false
        ).Value
        #$existing = $existing | ForEach-Object { if ($_ -notin $fakeBoundParameters) { $_ } }
        Compare-Object -PassThru $candidates $existing | Where-Object SideIndicator -eq '<='
    })]
[ValidateScript({
        if ($_ -notin [PolicyIDz]::new().GetValidValues()) { throw "Invalid policy ID: $_" }
        $true
    })]
[Parameter(Mandatory = $false, ParameterSetName = "Unsigned Or Supplemental")]
[System.String[]]$PolicyIDs,


[ArgumentCompleter({
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
        $candidates = [PolicyNamez]::new().GetValidValues() | ForEach-Object { $CurrentActiveLoop = $_; if ((((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.FriendlyName -eq $CurrentActiveLoop }).PolicyID) -notin $fakeBoundParameters) { $_ } }
        $existing = $commandAst.FindAll({ 
                $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
            }, 
            $false
        ).Value  
        # $existing = $existing | ForEach-Object { $CurrentActiveLoop = $_; if ((((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.FriendlyName -eq $CurrentActiveLoop }).PolicyID) -notin $fakeBoundParameters) { $_ } }
          (Compare-Object -PassThru $candidates $existing | Where-Object SideIndicator -eq '<=').
        ForEach({ if ($_ -match ' ') { "'{0}'" -f $_ } else { $_ } })
    })]
[ValidateScript({
        if ($_ -notin [PolicyNamez]::new().GetValidValues()) { throw "Invalid policy name: $_" }
        $true
    })]
[Parameter(Mandatory = $false, ParameterSetName = "Unsigned Or Supplemental")]
[System.String[]]$PolicyNames,

My goal was to cross-refence all of the values stored in the $fakeBoundParameters by policy ID. I'm not sure what I'm missing.

They use class based ValidateSets too

# argument tab auto-completion and ValidateSet for Policy names 
        Class PolicyNamez : System.Management.Automation.IValidateSetValuesGenerator {
            [System.String[]] GetValidValues() {
                $PolicyNamez = ((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }).Friendlyname | Select-Object -Unique
   
                return [System.String[]]$PolicyNamez
            }
        }

        # argument tab auto-completion and ValidateSet for Policy IDs     
        Class PolicyIDz : System.Management.Automation.IValidateSetValuesGenerator {
            [System.String[]] GetValidValues() {
                $PolicyIDz = ((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }).policyID
   
                return [System.String[]]$PolicyIDz
            }
        }    
    }

1

There are 1 best solutions below

0
On BEST ANSWER

Here is the answer, the code that makes it behave exactly like i want.

These are the argument completers

# https://stackoverflow.com/questions/76143006/how-to-prevent-powershell-validateset-argument-completer-from-suggesting-the-sam/76143269
        [ArgumentCompleter({
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

                # Get a list of policies using the CiTool, excluding system policies and policies that aren't on disk.
                $policies = (CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }
                # Create a hashtable mapping policy IDs to policy names. This will be used later to check if a policy name already exists.
                $IDNameMap = @{}
                foreach ($policy in $policies) {
                    $IDNameMap[$policy.policyID] = $policy.Friendlyname
                }
                # Get the names of existing policies that are already being used in the current command.
                $existingNames = $fakeBoundParameters['PolicyNames']
                # Get the policy IDs that are currently being used in the command. This is done by looking at the abstract syntax tree (AST)
                # of the command and finding all string literals, which are assumed to be policy IDs.
                $existing = $commandAst.FindAll({
                        $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
                    }, $false).Value
                # Filter out the policy IDs that are already being used or whose corresponding policy names are already being used.
                # The resulting list of policy IDs is what will be shown as autocomplete suggestions.
                $candidates = $policies.policyID | Where-Object { $_ -notin $existing -and $IDNameMap[$_] -notin $existingNames }
                # Return the candidates.
                return $candidates
            })]
        [ValidateScript({
                if ($_ -notin [PolicyIDzx]::new().GetValidValues()) { throw "Invalid policy ID: $_" }
                $true
            })]
        [Parameter(Mandatory = $false, ParameterSetName = "Unsigned Or Supplemental")]
        [System.String[]]$PolicyIDs,

        # https://stackoverflow.com/questions/76143006/how-to-prevent-powershell-validateset-argument-completer-from-suggesting-the-sam/76143269
        [ArgumentCompleter({
                # Define the parameters that this script block will accept.
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

                # Get a list of policies using the CiTool, excluding system policies and policies that aren't on disk.
                $policies = (CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }

                # Create a hashtable mapping policy names to policy IDs. This will be used later to check if a policy ID already exists.
                $NameIDMap = @{}
                foreach ($policy in $policies) {
                    $NameIDMap[$policy.Friendlyname] = $policy.policyID
                }

                # Get the IDs of existing policies that are already being used in the current command.
                $existingIDs = $fakeBoundParameters['PolicyIDs']

                # Get the policy names that are currently being used in the command. This is done by looking at the abstract syntax tree (AST)
                # of the command and finding all string literals, which are assumed to be policy names.
                $existing = $commandAst.FindAll({
                        $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
                    }, $false).Value

                # Filter out the policy names that are already being used or whose corresponding policy IDs are already being used.
                # The resulting list of policy names is what will be shown as autocomplete suggestions.
                $candidates = $policies.Friendlyname | Where-Object { $_ -notin $existing -and $NameIDMap[$_] -notin $existingIDs }

                # Additionally, if the policy name contains spaces, it's enclosed in single quotes to ensure it's treated as a single argument.
                # This is achieved using the Compare-Object cmdlet to compare the existing and candidate values, and outputting the resulting matches.
                # For each resulting match, it checks if the match contains a space, if so, it's enclosed in single quotes, if not, it's returned as is.
            (Compare-Object -PassThru $candidates $existing | Where-Object SideIndicator -EQ '<=').
                ForEach({ if ($_ -match ' ') { "'{0}'" -f $_ } else { $_ } })
            })]
        [ValidateScript({
                if ($_ -notin [PolicyNamezx]::new().GetValidValues()) { throw "Invalid policy name: $_" }
                $true
            })]
        [Parameter(Mandatory = $false, ParameterSetName = "Unsigned Or Supplemental")]
        [System.String[]]$PolicyNames,

These are the class-based argument-completers

# argument tab auto-completion and ValidateSet for Policy names
        # Defines the PolicyNamez class that implements the IValidateSetValuesGenerator interface. This class is responsible for generating a list of valid values for the policy names.
        Class PolicyNamez : System.Management.Automation.IValidateSetValuesGenerator {
            # Creates a static hashtable to store a mapping of policy IDs to their respective friendly names.
            static [Hashtable] $IDNameMap = @{}

            # Defines a method to get valid policy names from the policies on disk that aren't system policies.
            [System.String[]] GetValidValues() {
                $policies = (CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }
                self::$IDNameMap = @{}
                foreach ($policy in $policies) {
                    self::$IDNameMap[$policy.policyID] = $policy.Friendlyname
                }
                # Returns an array of unique policy names.
                return [System.String[]]($policies.Friendlyname | Select-Object -Unique)
            }

            # Defines a static method to get a policy name by its ID. This method will be used to check if a policy ID is already in use.
            static [System.String] GetPolicyNameByID($ID) {
                return self::$IDNameMap[$ID]
            }
        }

        # Defines the PolicyIDz class that also implements the IValidateSetValuesGenerator interface. This class is responsible for generating a list of valid values for the policy IDs.
        Class PolicyIDz : System.Management.Automation.IValidateSetValuesGenerator {
            # Creates a static hashtable to store a mapping of policy friendly names to their respective IDs.
            static [Hashtable] $NameIDMap = @{}

            # Defines a method to get valid policy IDs from the policies on disk that aren't system policies.
            [System.String[]] GetValidValues() {
                $policies = (CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }
                self::$NameIDMap = @{}
                foreach ($policy in $policies) {
                    self::$NameIDMap[$policy.Friendlyname] = $policy.policyID
                }
                # Returns an array of unique policy IDs.
                return [System.String[]]($policies.policyID | Select-Object -Unique)
            }

            # Defines a static method to get a policy ID by its name. This method will be used to check if a policy name is already in use.
            static [System.String] GetPolicyIDByName($Name) {
                return self::$NameIDMap[$Name]
            }
        }

These are the class-based validate sets (only used to validate the arguments)

# ValidateSet for Policy names 
        Class PolicyNamezx : System.Management.Automation.IValidateSetValuesGenerator {
            [System.String[]] GetValidValues() {
                $PolicyNamezx = ((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }).Friendlyname | Select-Object -Unique
   
                return [System.String[]]$PolicyNamezx
            }
        }

        # ValidateSet for Policy IDs     
        Class PolicyIDzx : System.Management.Automation.IValidateSetValuesGenerator {
            [System.String[]] GetValidValues() {
                $PolicyIDzx = ((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsOnDisk -eq "True" } | Where-Object { $_.IsSystemPolicy -ne "True" }).policyID
   
                return [System.String[]]$PolicyIDzx
            }
        }    

Fully tested it, works perfectly, it's for this sub-module https://github.com/HotCakeX/Harden-Windows-Security/blob/main/WDACConfig/Remove-WDACConfig.psm1