Given a function that has validation for a parameter:
function Test-Validation {
[CmdletBinding()]
param (
[Parameter()]
[ValidateScript({
# Add some validation that can throw.
if (-not (Test-Path -Path $_ -PathType Container)) {
throw "OutDir must be a folder path, not a file."
}
return $true
})]
[System.String]
$Folder
)
Process {
$Folder + " is a folder!"
}
}
We should be able to check the error type and set that as the ExpectedType in a Pester Test.
Test-Validation -Folder C:\Temp\file.txt
Test-Validation : Cannot validate argument on parameter 'Folder'. OutDir must be a folder path, not a file.
At line:1 char:17
+ Test-Validation C:\Temp\file.txt
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Test-Validation], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Validation
$Error[0].Exception.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False True ParameterBindingValidationException System.Management.Automation.ParameterBindingException
However, when testing in Pester, the test fails because it cannot find the type.
$ShouldParams = #{
Throw = $true
ExpectedMessage = "Cannot validate argument on parameter 'OutDir'. OutDir must be a folder path, not a file."
ExceptionType = ([System.Management.Automation.ParameterBindingValidationException])
}
{ Test-Validation -Folder C:\Temp\file.txt } | Should #ShouldParams
# Result
RuntimeException: Unable to find type [System.Management.Automation.ParameterBindingValidationException].
How can I fix this test so that I know I am not just catching any exception type?
To generalize your own answer a bit:
Since working with the types of objects starting from a given instance isn't that common in PowerShell, the fact that an instance's type may be non-public isn't usually obvious, as long as the type derives from (is a subclass of) the expected public type.
While you can obtain an object's non-public type via .GetType(), you cannot refer to it via a type literal (e.g. [System.Management.Automation.ParameterBindingException]), such as for use in Pester tests or parameter declarations.
You can call .GetType().IsPublic on any given instance to check whether its type is public, and .GetType().BaseType to get that type's base type - though you may have to call the latter multiple types until you reach a type for which .IsPublic is $true - see the convenience function at the bottom.
In the case at hand .GetType().BaseType.FullName is sufficient to reach the public base type:
# Provoke a non-public [System.Management.Automation.ParameterBindingValidationException]
# exception.
try { & { param([ValidateScript({ $false })] $foo)} bar } catch { $err = $_ }
# Output the full name of the exception type underlying the
# statement-terminating error that the failed validation reported:
$err.Exception.GetType().FullName
# It is only its *base* type that is public and therefore usable as a type
# literal ([...]), such as in a Pester test.
$err.Exception.GetType().BaseType.FullName
The above yields:
System.Management.Automation.ParameterBindingValidationException # non-public
System.Management.Automation.ParameterBindingException # public base type
Below is convenience function Get-PublicType, which, given any instance, reports the most derived type in the inheritance chain of the instance's type that is public (which may be the instance's type itself:
Sample call:
PS> Get-PublicType $err.Exception
PublicType NonPublicDerivedType Instance
---------- -------------------- --------
System.Management.Automation.ParameterBindingException {System.Management.Automation.ParameterBindingValidationException} System.Management.Automation.ParameterBindingValidationException: Cannot validate argument on par…
Get-PublicType source code:
function Get-PublicType {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
$Instance
)
process {
$type = $Instance.GetType()
$nonPublicTypes = #()
while (-not $type.IsPublic) {
$nonPublicTypes += $type
$type = $type.BaseType
}
# $type.FullName
[pscustomobject] #{
PublicType = $type
NonPublicDerivedType = $nonPublicTypes
Instance = $Instance
}
}
}
The reason why you cannot capture this type is because it is not a public class within [System.Management.Automation]. Instead you can set the -ExceptionType to the class it derives from [System.Management.Automation.ParameterBindingException] and your test will now pass with validation for the exception type thrown.
$ShouldParams = #{
Throw = $true
ExpectedMessage = "Cannot validate argument on parameter 'OutDir'. OutDir must be a folder path, not a file."
ExceptionType = ([System.Management.Automation.ParameterBindingException])
}
{ Test-Validation -Folder C:\Temp\file.txt } | Should #ShouldParams
Related
I have a script that calls other scripts that other people manage. It's essentially a CI/CD script that gives users the ability to tap into the pipeline.
The issue I'm running into now is that I would like this calling script to implement a couple new parameters. However, the old scripts don't always implement those parameters.
If I call their script that doesn't implement the parameters, I get an error "A parameter cannot be found that matches parameter name 'newparameter'".
Is there a way to dynamically pass in a parameter so that it doesn't fail if the parameter doesn't exist? I don't mind if they don't implement it. It's a bonus parameter that they don't need to use.
Alternately, can I do something like a Get-Command for a custom .ps1 script, to get a list of accepted parameters? With that, I could confirm that a parameter is implemented before I pass it.
This might help you get started, you could use the Parser Class
to get all functions and it's parameters from a script, this answer shows a minimal reproduction. I'll leave it to you to investigate further.
Given myScript.ps1 that has these 3 functions:
function ExampleFunc {
param([int] $param1 = 123, [string] $param2)
}
function ExampleFunc2 {
param([object] $param3, [switch] $param4)
}
function ExampleFunc3 ($param5, [hashtable] $param6 = #{foo = 'var'}) {
}
You can use the ParseFile Method to get the AST, then you can use the .FindAll method to filter for all FunctionDefinitionAst and subsequently find all parameters filtering for all ParameterAst.
using namespace System.Management.Automation.Language
$ast = [Parser]::ParseFile('path\to\myScript.ps1', [ref] $null, [ref] $null)
$ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true) | ForEach-Object {
$out = [ordered]#{ Function = $_.Name }
$_.FindAll({ $args[0] -is [ParameterAst] }, $true) | ForEach-Object {
$out['ParameterName'] = $_.Name.VariablePath
$out['Type'] = $_.StaticType
$out['DefaultValue'] = $_.DefaultValue
[pscustomobject] $out
}
} | Format-Table
Above code would result in the following for myScript.ps1:
Function ParameterName Type DefaultValue
-------- ------------- ---- ------------
ExampleFunc param1 System.Int32 123
ExampleFunc param2 System.String
ExampleFunc2 param3 System.Object
ExampleFunc2 param4 System.Management.Automation.SwitchParameter
ExampleFunc3 param5 System.Object
ExampleFunc3 param6 System.Collections.Hashtable #{foo = 'var'}
The same could be accomplished using Get-Command:
(Get-Command 'fullpath\to\myScript.ps1').ScriptBlock.Ast.FindAll({
... same syntax as before ... }, $true # or $false for non-recursive search
)
I am currently practicing the use of functions in PowerShell and am running into an error. I created the function below to accept DC Super Hero names and return the name of the hero being passed to the function.
function Get-DCHero {
[CmdletBinding()]
param (
[Parameter(Mandatory)][ValidateSet('Batman','Superman','Aquaman','Wonder Woman','Flash',ErrorMessage = "'{0}' is not a DC Super Hero. Please trying one of the following: '{1}'")]
[string]$Name
)
Write-OutPut "$Name is a DC Super hero."
}
As of now the function works properly without the ErrorMessage portion on the ValidateSet. When including the ErrorMessage portion I am receiving the following error:
Get-DCHero -Name
Property 'ErrorMessage' cannot be found for type 'System.Management.Automation.CmdletBindingAttribute'.
At C:\Users\AAP8801\DCSuperHero.ps1:5 char:98
+ ... n','Flash', ErrorMessage = "'{0}' is not a DC Super Hero. Please tryi ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (ErrorMessage = ...llowing: '{1}'":NamedAttributeArgume
ntAst) [], RuntimeException
+ FullyQualifiedErrorId : PropertyNotFoundForType
When a parameter is passed to the function that is not part of the validation set I would like to able to edit the error message being throw. Can anyone tell me why I am unable to do this successfully?
To complement Santiago Squarzon's helpful answer by spelling out the [ValidateScript()] workaround he mentions, which is slightly easier than defining a custom attribute class:
function Get-DCHero {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript({
$set = 'Batman','Superman','Aquaman','Wonder Woman','Flash'
if ($_ -in $set) { return $true } # OK
throw "'$_' is not a DC superhero. Please try one of the following: '$($set -join ',')'"
})]
[string]$Name
)
"$Name is a DC superhero."
}
As Mathias explained in comments, the ErrorMessage property is available on the PowerShell SDK v6.2.0 and above.
As a workaround in Windows PowerShell, you could use the ValidateScript Attribute Declaration or you could create your own attribute declaration by inheriting from ValidateEnumeratedArgumentsAttribute, Base Type of the ValidateSet Class.
The following example can help you get started, and if you're interested in learning more, I would recommend you these nice articles from Kevin Marquette:
Powershell: Creating and using custom attributes
Powershell: Creating parameter validators and transforms
Class Definition
using namespace System.Management.Automation
class MyDCHeroSet : ValidateEnumeratedArgumentsAttribute {
[scriptblock] $Set
[string] $ErrorMessage
[void] ValidateElement([object] $Object) {
$ValidValues = & $this.Set
if($Object -notin $ValidValues) {
throw [ValidationMetadataException]::new(
[string]::Format(
$this.ErrorMessage,
$Object, ($ValidValues -join ',')
)
)
}
}
}
Implementation and Testing
function Get-DCHero {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[MyDCHeroSet(
Set = { 'Batman', 'Superman', 'Aquaman', 'Wonder Woman', 'Flash' },
ErrorMessage = "'{0}' is not a DC Super Hero. Please trying one of the following: '{1}'"
)]
[string] $Name
)
Write-Output "$Name is a DC Super hero."
}
Get-DCHero -Name Spiderman
I am generating a ScriptBlock based on DB input which I invoke later in the script. I now want to ensure that a malicious user is not injecting any PS code in the DB varchar field that then gets executed.
First, I filtered the String Script Block for forbidden chars such as $ or ;. But I want to take it one step further and use AST to check if there is any executable code in the DB field.
When I use $Ast.FindAll for a specific element such as ForEachStatementAst it works fine.
However, I also want to be able to detect cmdlets etc in the String.
Examples that should be recognised as being ok:
abc
123
'a','b'
true
Examples that should be recognised as being not ok:
Write-host or Remove-Item or any other get-command cmdlet.
`$(MySubExpression)
When using AST visualisation, I get the same tree for both examples. ('abc', 'Write-Host')
ScriptBlockAst-> NamedBlockAst -> PipelineAst -> CommandAst -> StringConstantExpressionAst
Is there any way I can use AST to determine whether the DB field (or any string) contains only allowed entries such as non PS keywords / cmdlets, numbers etc but nothing that could be used as a PS command and that could be invoked?
The following code works for the test cases but I wonder if this can be achieved in a better way. If Res.count > 0, the input was not ok, if =0, it was ok.
$DebugPreference = 'Continue'
[System.Collections.Generic.List[System.String]]$InputStringList = New-Object -TypeName "System.Collections.Generic.List[System.String]"
$InputStringList.Add("foreach (`$x in #('a','b')){;}")
$InputStringList.Add("New-item -Path 'C:\Test.txt' -ItemType File")
$InputStringList.Add("Write-host 'as'")
$InputStringList.Add("abc")
$InputStringList.Add("a,b,c")
$InputStringList.Add("123")
$InputStringList.Add("true")
[System.Collections.Generic.List[System.Type]]$TypeList = New-Object -TypeName "System.Collections.Generic.List[System.Type]"
$TypeList.Add([System.Management.Automation.Language.StringLiteralToken])
$TypeList.Add([System.Management.Automation.Language.ScriptBlockAst])
$TypeList.Add([System.Management.Automation.Language.NamedBlockAst])
$TypeList.Add([System.Management.Automation.Language.StringConstantExpressionAst])
$TypeList.Add([System.Management.Automation.Language.ConstantExpressionAst])
$TypeList.Add([System.Management.Automation.Language.CommandExpressionAst])
$TypeList.Add([System.Management.Automation.Language.CommandAst])
$TypeList.Add([System.Management.Automation.Language.PipelineAst])
$TypeList.Add([System.Management.Automation.Language.ArrayLiteralAst])
[String[]]$CommandArray = (Get-Command | Select-Object -ExpandProperty 'Name')
[System.Management.Automation.ScriptBlock]$Predicate =
{
param([System.Management.Automation.Language.Ast]$AstObject)
Write-Debug -Message $AstObject.GetType().FullName
if($AstObject -is [System.Management.Automation.Language.StringConstantExpressionAst])
{
if($AstObject.Value -in $CommandArray)
{
return $true
}
else
{
return $false
}
}
else
{
return (-not($AstObject.GetType() -in $TypeList))
}
}
$InputStringList.GetEnumerator() | ForEach-Object -Process `
{
Write-Debug -Message ("Processing string: "+$PsItem.ToString())
$ast = [System.Management.Automation.Language.Parser]::ParseInput($PsItem, [ref]$null, [ref]$null)
$res=$ast.FindAll($Predicate, $true)
Write-Debug -Message $res.count.ToString()
}
As commented, what you trying to do is creating your own restricted languagemode. Meaning that it would probably be easier to invoke the concerned scriptblock in an restricted runspace.
Derived from #mklement0 great answer for Automatically retrieve Allowed Types for Constrained Language mode:
Function Invoke-Restricted {
[CmdletBinding()]param([String]$Expression)
$Restricted = [powershell]::Create()
$Restricted.Runspace.SessionStateProxy.LanguageMode = 'Restricted'
Try { $Restricted.AddScript($expression).Invoke() }
Catch { $PSCmdlet.ThrowTerminatingError($_) }
}
Restricted expression
Invoke-Restricted #'
#{
string = 'abc'
int = 123
array = 'a','b'
hashtable = #{ a = 1; b = 2 }
boolean = $true
}
'#
Yields
Name Value
---- -----
array {a, b}
int 123
boolean True
string abc
hashtable {b, a}
Invalid expression
Invoke-Restricted #'
#{
TimeSpan = [TimeSpan]'12:34:45'
}
'#
Throws an error:
Invoke-Restricted: Exception calling "Invoke" with "0" argument(s): "At line:1 char:1
+ [TimeSpan]"12:34:45"
+ ~~~~~~~~~~
The type TimeSpan is not allowed in restricted language mode or a Data section."
Yet, it has some limitations as it does not prevent e.g. the use of cmdlets.
For an easy and secure way to retrieve a (structured) configuration file I would depend on a serialized format as JSON using the ConvertFrom-Json cmdlet
Related: #12377 Running partly trusted PowerShell code in a restricted security environment.
So I have a powershell script that takes region and datetime as input and internally in the script it calls a stored procedure.
The stored procedure takes two inputs - region and datetimestamp; the datetimestamp is always null. How do I go about parsing it?
function Reset {
param ([string]$region,[nullable[Datetime]]$statustimestamp)
$conn = New-Object System.Data.SqlClient.SqlConnection("Server=SQL,15010; Database='STG';User ID=SVC;Password=password;Integrated Security=FALSE")
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = "dbo.Reset'$region' ,'$statustimestamp'"
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
$dataset = New-Object System.Data.DataSet
[void]$adapter.Fill($dataset)
$dataset.tables[0]
$cmd.CommandText
$dataset.Tables[0] | Export-CSV M:\MyReport.csv -encoding UTF8 -NoTypeInformation
Write-Host 'New report M:\MyReport.csv has been successfully generated'
}
I execute it as
Rest -region IN -statustimestamp NULL
and I get the following error
Reset : Cannot process argument transformation on parameter 'statustimestamp'. Cannot convert value "NULL" to type "System.DateTime".
Error: The string was not recognized as a valid DateTime. There is an unknown word starting at index 0.
At line:1 char:59
+ Reset -region AU -statustimestamp NULL
+ ~~~~
+ CategoryInfo : InvalidData: (:) [Reset], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Reset
To complement dee-see's helpful answer:
Note: However your parameters are declared, $PSBoundParameters.ContainsKey('<parameter-name>') inside a script/function tells you whether an argument was explicitly passed to a given parameter (a default value doesn't count); e.g., with the invocation in your question (had it succeeded), $PSBoundParameters.ContainsKey('statustimestamp') would indicate $true.
If you want your parameter value to be $null by omission:
Declare your parameter simply as [Datetime] $statustimestamp and pass no argument to it on invocation; $statustimestamp will then implicitly be $null.
# Declare a parameter in a script block (which works like a function)
# and invoke the script block without an argument for that parameter:
PS> & { param([Datetime] $statustimestamp) $null -eq $statustimestamp }
True # $statustimestamp was $null
If you want to support explicitly passing $null as an argument:
This may be necessary if you declare a mandatory parameter, yet you want to allow $null as an explicit signal that a default value should be used.
Unfortunately, the specifics of the parameter declaration currently depend on whether the data type of the parameter is a reference type (such as [string] or [System.IO.FileInfo]) or a value type (such as [int] or [datetime]).
You can inspect a given type's .IsValueType property to learn whether it is a value type ($true) or a reference type ($false); e.g.: [datetime].IsValueType yields $true).
If the parameter type is a reference type, you can use the [AllowNull()] attribute:
PS> & {
param(
[AllowNull()]
[Parameter(Mandatory)]
[System.IO.FileInfo] $Foo # System.IO.FileInfo is a *reference type*
)
$null -eq $Foo
} -Foo $null
True # $Foo was $null
Unfortunately, the same technique doesn't work with value types such as [DateTime], so your parameter must indeed be typed as [Nullable[DateTime], as in your question:
PS> & {
param(
[Parameter(Mandatory)]
[AllowNull()] # Because the parameter is mandatory, this is *also* needed.
[Nullable[DateTime]] $Foo # System.DateTime is a *value type*
)
$null -eq $Foo
} -Foo $null
True # $Foo was $null
Note: These requirements - needing to pay attention to the difference between value types and reference types and needing to use a [Nullable[T]] type - are obscure and uncharacteristic for PowerShell.
Doing away with these requirements in favor of a unified approach (making it work for value types the way it already does for reference types) is the
subject of this proposal on GitHub.
Null in PowerShell is represented by $null and not NULL, that's why the error message is saying the string NULL cannot be converted to a (nullable) DateTime.
Rest -region IN -statustimestamp $null
You can also omit the -statustimestamp parameter altogether.
I am trying to create function that will receive an argument as log file path.
I want this parameter to be able to receive string path or used as a [switch], without any arguments.
The reason I want to do it is because I have three scenarios I need to cover and I wanted to do it with only one parameter:
1. The parameter is not passed
2. The parameter is passed with empty argument
3. The parameter is passed with argument
Here is a script that demonstrate what I want:
function myFunc(){
param(
$LogFile = $null
)
# 1. PS > myFunc
if($LogFile -eq $null){
}
# 2. PS > myFunc -LogFile
if($LogFile -eq ""){
}
# 3. PS > myFunc -LogFile "C:\tmp\log.txt"
else{
}
}
Is it possible to create such input parameter that can receive empty and non-empty values ?
When I run myFunc -LogFile I receive an error:
myFunc : Missing an argument for parameter 'LogFile'. Specify a parameter of type 'System.Object' and try again.
At line:1 char:8
+ myFunc -LogFile
+ ~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [myFunc], ParameterBindingException
+ FullyQualifiedErrorId : MissingArgument,myFunc
This is because it is not set to [switch] and if I add the [switch] than I can't run myFunc -LogFile c:\tmp\file.txt:
myFunc : A positional parameter cannot be found that accepts argument 'C:\tmp\file.txt'.
At line:1 char:1
+ myFunc -LogFile C:\tmp\file.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [myFunc], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,myFunc
I also tried to use [AllowNull()] and [AllowEmptyString()] but they still require to some char.
More information about the parameters can find here.
I don't think you can have a parameter acting like a switch as well as like string input. This is because if you try to use it like a switch it will complain that you didn't pass it any input:
myFunc : Missing an argument for parameter 'LogFile'. Specify a
parameter of type 'System.Object' and try again.
One way to get the behavior you're looking for would be to have $LogFile as a switch and then have a second parameter that could take the string (which you wouldn't need to explicitly declare as the input would go to this parameter by the order its called in -- giving the impression it's being passed to -LogFile). Here's an example of that:
function myFunc{
param(
[switch]$LogFile,
[string]$LogPath = $null
)
# 1. PS > myFunc
if ($LogFile -eq $false){
"Function used without parameter"
}
# 2. PS > myFunc -LogFile
elseif ($LogFile -eq $true -and -not $LogPath){
"Parameter used like a switch"
}
# 3. PS > myFunc -LogFile "C:\tmp\log.txt"
else{
"Parameter was $LogPath"
}
}
myfunc
myfunc -logfile
myfunc -logfile "C:\tmp\log.txt"
To be honest though, scernario 2 is sort of not necessary on the assumption that all you want to do in this case is have a default path. In which case, you could just give $LogPath a default in the param() block. It's also probably not a good idea to be misleading about what is actually going on with the parameters (although the get-help documentation syntax block would expose that regardless).
Accept the $LogFile as a switch argument, then if it's present, just convert it's type to a string value, populating it with the value of the following argument, if present:
param(
[switch]
$LogFile
)
if($LogFile)
{
#We neeed to change the variable type, so remove it
Remove-Variable -Name "LogFile"
#The remaining arguments are placed in $args array.
if($args.Count -gt 0)
{
$LogFile = [String]$args[0]
}
else {
$LogFile = [String]""
}
}
# 1. PS > myFunc
else {
"# 1. PS > myFunc "
}
# 2. PS > myFunc -LogFile
if($LogFile -eq ""){
"LogFile is empty"
}
# 3. PS > myFunc -LogFile "C:\tmp\log.txt"
elseif ($LogFile -is [String]){
"logFile is $logFile"
}
function cookieMonster($ChocolateChipCookie){
if ([string]::IsNullOrEmpty($chocolateChipCookie)){
write-host "Hey wheres my Cookies! "
}
else {
write-host "MMMMM COOKIES! NOM NOM NOM"
}
}
cookieMonster -ChocolateChipCookie C:\cookiejar\cookie.txt
This answer is inspired by the Legendary Mark Wragg above:
function cookieMonster{
param(
[switch] $ChocolateChipCookie,
[string] $Crumbs = $null
)
# The function without parameters
if($ChocolateChipCookie -eq $false){
write-host "no cookies;("
}
# The parameter is not passed
elseif ($ChocolateChipCookie -eq $true -and -not $Crumbs) {
Write-Host "Aw just crumbs!"
}
# The parameter is passed with argument
else {
write-host "MMMMM COOKIES! NOM NOM NOM"
}
}
cookieMonster -ChocolateChipCookie C:\cookiejar\cookie.txt
The $ChocolateChipCookie is defined as a switch parameter.
Then the value given to the switch parameter is created as string parameter called $crumbs.
The if block checks if the function was called without a switch and writes a message.
The elseif block checks if the function was called with a switch which has no value.
The else block writes a message if non of the above conditions are met.