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
}
}
}
Here is the answer, the code that makes it behave exactly like i want.
# 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,
# 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]
}
}
# 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