Update KB2928680 Available to Fix Invalid Properties error when using Get-ADUser/Computer

For those of you running Windows 8.1 or Windows 2012 R2, you may have seen issues where using Get-ADComputer and Get-ADUser with the –Property parameter and specifying * to grab all properties of the object.

Get-ADUser proxb -Server DC1 -Property *

The error that occurs is:

Get-ADUser : One or more properties are invalid.
Parameter name: msDS-AssignedAuthNPolicy
At line:1 char:1
+ Get-ADUser proxb -Server DC1 -Properties *
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (proxb:ADUser) [Get-ADUser], ArgumentException
    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:System.ArgumentException,Microsoft.ActiveDirec
   tory.Management.Commands.GetADUser

 

Not exactly useful if you need to see all of the properties of the AD object. Fortunately, Microsoft has released KB2928680 which includes a number of fixes, but one does stand out for me at least:

  • 2923122 One or more properties are invalid error when you run the Get-ADUser or Get-ADComputer cmdlet

Applying this update will fix this issue as demonstrated below:

Get-ADUser proxb -Server DC1 -Property *

image

This update is available via Windows Updates, or your WSUS server if running a server in your environment. A standalone installer is also available here.

Posted in News, powershell | Tagged , , , | Leave a comment

WSUS Reporting: Digging Into Target Groups and Update Statuses Using PowerShell

I had an email recently asking how to report on the number of updates per Target Group. Basically when a report is run using the WSUS Management Console, you can create a tabular report that displays something similar to this:

image

By specifying a Target Group and then report on all systems with the number of updates Needed, Installed/NotApplicable, Failed and NoStatus. Not too difficult to do with WSUS, but if you want to work with multiple target groups then you really are not sure what group belongs to what systems.

In the end, I wrote a fairly short script that the individual can use to get the information he needed. First I wanted to just show the number of updates per Target Group, then dive into a specific target group and pull number of updates per Computer (like the report above) and present it out. Lastly, I wanted to go even deeper into the rabbit hole and just pull updates which are Needed by a specific computer and list those out.

Before I do anything else, I need to make my initial connection using the WSUS API (WSUS Management Console is required for access).

$Computername = 'dc1'
$UseSSL = $False
$Port = 80

[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
$Wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($Computername,$UseSSL,$Port)

First, lets take a look at the Target Groups:

#Updates per TargetGroup
$wsus.GetComputerTargetGroups() | ForEach {
    $Group = $_.Name
    $_.GetTotalSummary() | ForEach {
        [pscustomobject]@{
            TargetGroup = $Group
            Needed = ($_.NotInstalledCount + $_.DownloadedCount)
            "Installed/NotApplicable" = ($_.NotApplicableCount + $_.InstalledCount)
            NoStatus = $_.UnknownCount
            PendingReboot = $_.InstalledPendingRebootCount
        }
    }
}

 

image

One thing that I have done better all ready with the report is that I can see what how many updates require a reboot.

Now onto the Windows Server 2003 Target Group. Let’s see how many updates are needed per computer (in this case only 1 system).

image

Lastly, I want to dive even deeper into this and see what updates exactly are needed for my server before I go about approving them for installation.

$TargetGroup = 'Windows Server 2003'
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.IncludedInstallationStates = 'Downloaded','NotInstalled'
($wsus.GetComputerTargetGroups() | Where {
    $_.Name -eq $TargetGroup 
}).GetComputerTargets() | ForEach {
        $Computername = $_.fulldomainname
        $_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach {
            $update = $_.GetUpdate()
            [pscustomobject]@{
                Computername = $Computername
                TargetGroup = $TargetGroup
                UpdateTitle = $Update.Title 
                IsApproved = $update.IsApproved
            }
    }
}

image

Note my use of a UpdateScope object that is used to provide the necessary information for the InstallationStates (Downloaded, NotInstalled) so I can be sure to get an accurate report.

Now we saw that 1 update was pending a reboot, so the question is: what update was installed that is awaiting a system reboot? Let’s find out!

$TargetGroup = 'Windows Server 2003'
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.IncludedInstallationStates = 'InstalledPendingReboot'
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerScope.IncludedInstallationStates = 'InstalledPendingReboot'
($wsus.GetComputerTargetGroups() | Where {
    $_.Name -eq $TargetGroup 
}).GetComputerTargets($computerScope) | ForEach {
        $Computername = $_.fulldomainname
        $_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach {
            $update = $_.GetUpdate()
            [pscustomobject]@{
                Computername = $Computername
                TargetGroup = $TargetGroup
                UpdateTitle = $Update.Title 
                IsApproved = $update.IsApproved
            }
    }
} 

image

The only adjustment here was that I only had one InstallationState used (InstalledPendingReboot). I also created a ComputerTargetScope and applied the InstallationState of InstalledPendingReboot to better filter for only the computers that are in a pending reboot state. The rest was the same code I had used before. The nice thing about this is you don’t have to filter by target group or computername and just pull from all systems to see any computers which are awaiting a reboot and what update is requiring it.

Posted in powershell, WSUS | Tagged , , , , | 18 Comments

Building a TCP Server Using PowerShell

Something that I have been working on for the past week or so is building a TCP server that I can use to issue commands from remotely and have it carry out on the remote server.

I’ve already written a PowerShell Chat server and chronicled that build, but this is something a little different. I will not be handling more than one client connection and I will instead be running commands on the remote system based on what I send to the open port.

Before I begin, I need to lay out some requirements that I want to hit if I trust that this will be usable in something other than a lab environment.

  • Provide some sort of authentication mechanism
    • Also want to impersonate remote client token to run commands
  • Capable of returning actual objects from remote system (serialization)
  • Handle a single connection and then close connection after the command issued and output returned to client

With that in mind, I can now start out by initializing my server. Note that I will be jumping back and forth between a Client and Server with my code examples. I am also using two separate systems to show a more realistic approach as to how this works.

Serialization of Data

Before I dive into the TCP side of the house, I want to quickly cover what how we are going to receive the output data from the remote system over the network. If you work with PowerShell remoting at all, you know that serialization plays a major role in receiving the data from a remote system.

In PowerShell V3+, we have the [System.Management.Automation.PSSerializer] class publicly available to us and the appropriate Serialize() and Deserialize() methods available to us to transform the data into XML prior to shipping across the network.

$data = Get-ChildItem -File | 
Select -First 1 -Property FullName, Length, LastWriteTime

$serialized = [System.Management.Automation.PSSerializer]::Serialize($Data)

$serialized

 

image_thumb11

$deserialized = [System.Management.Automation.PSSerializer]::Deserialize($serialized)
$deserialized

image_thumb13

Of course, there are distinct possibilities that systems are still running PowerShell V2 in which case these are not publicly available. Using Reflection, we can still access the methods to serialize and deserialize the data. Fortunately, the PowerShell Community has done the work for us and has two functions (ConvertTo-CliXml (Original Author Oisin Grehan <Twitter | Blog>; Current Version Joel Bennett <Twitter | Blog> ) and ConvertFrom-CliXml (Original Author David Sjstrand; Current Version Joel Bennett)) readily available to use. The remote server that I am using for my demo only has PowerShell V2 on it, so I will be making use of ConvertTo-CliXml.

Server

First it is time to initialize the port listener on the server so we can start accepting connections.

First let’s check to see if the port is opened.

image

Nothing opened, now lets open that port up.

##Server
[console]::Title = ("Server: $env:Computername <{0}> on $port" -f `
[net.dns]::GetHostAddresses($env:Computername))[0].IPAddressToString
$port=1655
$endpoint = new-object System.Net.IPEndPoint ([system.net.ipaddress]::any, $port)
$listener = new-object System.Net.Sockets.TcpListener $endpoint
$listener.start()
$client = $listener.AcceptTcpClient()

image

image

This is a blocking method meaning that until something makes a connection, this will prevent me from accessing the console.

Now that we are listening, lets make an initial connection from the client side.

Client

##Client
[console]::Title = ("Server: $env:Computername <{0}>" -f `
[net.dns]::GetHostAddresses($env:Computername))[0].IPAddressToString
$port=1655
$server='Boe-PC'
$client = New-Object System.Net.Sockets.TcpClient $server, $port

image

Connection has been made. We can verify on the server by seeing if the console has opened up and also by running another netstat.

image

So what happens now? Here is where we make the decision to use a NegotiateStream vs. a regular network stream. By using a NegotiateStream, we will be able to then provide an Authentication Stream that will be used to transfer client and server authentication data between the two as well as being able to sign and encrypt the data transmission. By using a network stream, anonymous users could easily connect to the remote system and issue commands as the user that started the listener! Not exactly what you want to deal with.

$stream = $client.GetStream()
$NegotiateStream =  New-Object net.security.NegotiateStream -ArgumentList $stream

First I get the network stream by calling the GetStream() method and then use that in the construction of the NegotiateStream object.

Before I do anything else, I need to kick off the same stream on the server.

Server

$stream = $client.GetStream()
$NegotiateStream =  New-Object net.security.NegotiateStream -ArgumentList $stream
#Validate Alternate credentials
Try {
    $NegotiateStream.AuthenticateAsServer(
        [System.Net.CredentialCache]::DefaultNetworkCredentials,
        [System.Net.Security.ProtectionLevel]::EncryptAndSign,
        [System.Security.Principal.TokenImpersonationLevel]::Impersonation
    )
    Write-host "$($client.client.RemoteEndPoint.Address) authenticated as $($NegotiateStream.RemoteIdentity.Name) via $($NegotiateStream.RemoteIdentity.AuthenticationType)" -Foreground Green -Background Black
} Catch {
    Write-Warning $_.Exception.Message
}

Same as with the client, I have to construct the NegotiateStream object using the network stream. After that it is time to accept an authentication request from the client.

There are 4 possible parameter sets with the AuthenticateAsServer() method. In this instance, I am choosing to supply a set of default credentials, making sure that I encrypt and sign the data being transferred and declaring that I will only accept Impersonation as my token impersonation level. Attempts at negotiating anything else will end up with a denied connection.

image

Once I call this method, it becomes a blocking call until I attempt authentication from my client.

Client

I am now going to use AuthenticateAsClient() to try to pass some alternate credentials (proxb) to the server in hopes of being let in.

Try {
    $NegotiateStream.AuthenticateAsClient(
        (Get-Credential).GetNetworkCredential(),
        'MYSERVICE\boe-pc',
        [System.Net.Security.ProtectionLevel]::EncryptAndSign,
        [System.Security.Principal.TokenImpersonationLevel]::Impersonation
    )
} Catch {
    Write-Warning $_.Exception.Message
}

 image

The AuthenticateAsClient() method is similar to what we used with the Server. It has multiple parameter sets including one where you supply no parameters. Because I want to make sure to negotiate Impersonation with the server, I am making sure to supply my default network credential, a SPN that I made up, EncryptAndSign so the data is transmitted securely and finally my TokenImpersonationLevel as Impersonate.

image

Inspecting the NegotateStream object, you can see that IsEncrypted and IsSigned is True which is what we wanted as well as the ImpersontationLevel is set to Impersonate. Diving deeper into the object via the RemoteIdentity property, we can see that we are using the SPN that we created as well as the AuthentcationType, in this case NTLM.

Server

On the server side, we will also inspect both the NegotiateStream and the RemoteIdentity property as well to see what they look like.

image

You can see by the Write-Host that I included that the remote client successfully connected as Boe-PC\proxb and that the authentication type was NTLM.

Same information on the NegotiateStream as what was one the client. But the RemoteIdentity property is much different. Here we can see the remote user’s name, the token being used as well as the group memberships (shown as a SID) that this user belongs to in relation to this server.

At this point, I could check to see if the remote user has access and make a decision to allow the connection or not. Something like this could be done:

([Security.Principal.WindowsPrincipal]$NegotiateStream.RemoteIdentity).IsInRole('Administrators') 

image

From here I could make a decision based on whether it comes back as True or False to continue allowing the connection or halt it. In this case, I would allow it to continue on.

So now that we have a good connection along with proper authentication, we can continue on with the connection and begin with attempting to impersonate the remote client.

Lets check out who the current user is on the server via the TCP server.

[System.Security.Principal.WindowsIdentity]::GetCurrent()

image

As you can see, I am currently running as my Administrator account (smart, right?) that launched the server. Now I will attempt to impersonate the remote client (proxb) and see what happens.

$remoteUserToken = $NegotiateStream.RemoteIdentity.Impersonate()

image

I saved the output object (System.Security.Principal.WindowsImpersonationContext) as a variable because this will be invaluable later on when I need to stop impersonating the remote client.

[System.Security.Principal.WindowsIdentity]::GetCurrent()

image

As you can see, I am now running under the boe-pc\proxb account after a successful impersonation! From here, depending on the rights that you have on the system, you can run commands as the user that you impersonated. Of course, if you have little to no rights, it will be quite difficult to do much of anything.

Client

Now that we have accomplished the connection and impersonation on the server, I will issue a command that will be run on the remote system and return the results back to the client.

$data = [text.Encoding]::Ascii.GetBytes('Get-WMIObject Win32_OperatingSystem | Select __Server, Caption')
Write-Verbose "Sending $($Data.count) bytes"
$NegotiateStream.Write($data,0,$data.length)
$NegotiateStream.Flush()

image

All that I’ve done is taken the command as a string and converted into bytes using the GetBytes() method before sending across the network via the NegotiateStream over to the remote system.

Server

First I will check to see if any data is available:

$Stream.DataAvailable

image

I know that there is data available so now I need to begin work to get the data and convert it back into something usable.

    $stringBuilder = New-Object Text.StringBuilder
    Do {
        [byte[]]$byte = New-Object byte[] 1024
        Write-Verbose ("{0} Bytes Left" -f $client.Available)
        $bytesReceived = $NegotiateStream.Read($byte, 0, $byte.Length)
        If ($bytesReceived -gt 0) {
            Write-Verbose ("{0} Bytes received" -f $bytesReceived)
            [void]$stringBuilder.Append([text.Encoding]::Ascii.GetString($byte[0..($bytesReceived - 1)]))
        } Else {
            $activeConnection = $False
            Break
        }  
    } While ($Stream.DataAvailable)

image

I use the StringBuilder class to handle the reconstruction of the command and continue with a Do loop until all of the data has been received from the remote client. You might be asking why 82 bytes were sent and only 62 bytes are shown to have been received. This is due to the  encryption that is being applied prior to sending the data across the network.

With the StringBuilder object, we have to cast it out to a string to see the data inside of it.

$stringBuilder.ToString()

image

There is our command that was sent from the remote client all ready for us to run here on the remote system. Speaking of which, lets continue on and run this command and save the results.

$string = $stringBuilder.ToString()
Write-Verbose ("Message received from {0} on {1}:`n{2}" -f $client.client.RemoteEndPoint.Address,
([System.Security.Principal.WindowsIdentity]::GetCurrent()).Name,
$string)
Try {      
    $ErrorActionPreference = 'Stop'           
    $Data = [scriptblock]::Create($string).Invoke()
} Catch {
    $Data = $_.Exception.Message
}
If (-Not $Data) {
    $Data = 'No data to return!'
}
Try {
    $ErrorActionPreference = 'stop'
    $serialized = [System.Management.Automation.PSSerializer]::Serialize($Data)
} Catch {
    $serialized = $data | ConvertTo-CliXml
 
}

 

image

I use [scriptblock]::Create() to build out the command and then use Invoke() to run the command. It’s really not much better than using Invoke-Expression. With either of these, you need to be mindful of possible code injections.

From there, I need to serialize the object that is being returned so I can send it over to the remote client. First I try the Serialize() method and if that fails (I temporarily set the $erroractionpreference to Stop to make sure the error is terminating), then default to the ConvertTo-CliXml function. The resulting data prior to being sent looks like this:

image

Next up is to convert that data into bytes and send it back to the client.

$ErrorActionPreference = 'Continue'
#Resend the Data back to the client
$bytes  = [text.Encoding]::Ascii.GetBytes($serialized)
 
#Send the data back to the client
Write-Verbose ("Sending {0} bytes" -f $bytes.count) -Verbose
$NegotiateStream.Write($bytes,0,$bytes.length)
$NegotiateStream.Flush() 

image

Client

I am basically going to repeat the same process on the client that I did on the server in checking for any data and then pulling it down as bytes and converting it to a string.

$stringBuilder = New-Object Text.StringBuilder
While ($client.available -gt 0) {
    Write-Verbose "Processing Bytes: $($client.Available)" -Verbose
    #$clientstream = $TcpClient.GetStream()
    [byte[]]$inStream = New-Object byte[] $client.Available
    $buffSize = $client.Available
    $return = $NegotiateStream.Read($inStream, 0, $buffSize)
    [void]$stringBuilder.Append([System.Text.Encoding]::ASCII.GetString($inStream[0..($return-1)]))   
}

image

Let’s verify that the data came across.

image

Now we need to deserialize the data so it is an actual object (PowerShell loves objects! Smile).

Try {
    $ErrorActionPreference = 'stop'
    $deserialized = [System.Management.Automation.PSSerializer]::DeSerialize($stringbuilder.ToString())
} Catch {
    $deserialized = $stringbuilder.ToString() | ConvertFrom-CliXml
 
}
$ErrorActionPreference = 'Continue'

Now we can look at the finished product.

image

Bear in mind that this is a deserialized object, so you will only have properties and a few select methods such as ToString() available.

image

Now that I am done with my connection, I need to clean up after myself.

Client

$stream.Close()
$client.Close()
$NegotiateStream.Close()
$stream.Dispose()
$client.Dispose()
$NegotiateStream.Dispose()

Server

$listener.Stop()
$reader.Dispose()
$stream.Dispose()
$client.Dispose()
$NegotiateStream.Dispose()
$remoteUserToken.Undo()
$remoteUserToken.Dispose()

A More Functional Way to Do This

Working with either of these (client or server) requires a lot of monitoring and manual commands to make sure that you are tracking what is being sent and received between these two connections. Fortunately, I have written a couple of functions that will make this much easier to manage.

First we need to load the module up.

Import-Module .\TCPServer.psm1 -Verbose

image

Invoke-TCPServer

The first function is called Invoke-TCPServer which can be used to either start a TCP server on a local or remote system. You can specify a Computername, Port and a Credential to run the TCP server as (also useful with remote systems that you are starting the server on).

This will start the TCP server and handle one connection at a time as well as using the negotiate stream that I have discussed here. After each connection and command ran based on the client, it will drop the client connection and force another reconnect.

An object is returned when you use the function showing the Computername, Port, ProcessID of the local/remote process being used for the server as well as a port check attempt (an initial warning will display on the server window because it will try to authenticate against the port check; this can be ignored) that you can then reference to track the TCP server.

Invoke-TCPServer -Computername 'boe-pc' -Port 1655 `
-Credential 'boe-pc\proxb' -Verbose

image

The IsPortAvailable is kind of hit or miss (I may pull this property in the next release), but the TCP server is now up and running on the remote system. I can verify on the remote system just to be sure.

image

For the sake of demonstrating and showing the window, I am going to run the server locally so the window is visible.

image

Again, the Warning is more of a friendly warning due to the port check that occurs after starting the process.

Send-Command

This now leads into my second function, called Send-Command. This does exactly what the function says, sends a command to the remote system and then waits for a response and displays the response (usually an object) to the client console.

Using my currently running server, lets send a simple command to it and see what happens!

Send-Command -Computername 'boe-pc' -Port 1655 `
-Command 'Get-WMIObject Win32_OperatingSystem | Select __Server, Caption' `
-Verbose -Credential 'boe-pc\proxb'

Capture

Works rather well. On the client piece, we can see that it attempts authentication and then sends its data to the remote server and waits for a response. Once the response has been received, it presents the data on the console. Now let’s look at the server side.

image

As you can see, the server took the command, ran it and sent the returned object back to the client. After sending the data back to the client, the connection to the client is closed.

The download link for these two functions are below. Note that these are still Proof of Concept and while they seem like a secure method, it should still be used with caution, especially if used in production. I have plans for more things to add to this, but for now, this is what it is. Feel free to let me know of any bugs or things that you would like to see added.

Download TCP Server Module

Technet Script Repository

Posted in powershell | Tagged , , | 7 Comments

PoshWSUS Update to 2.2.1

After a little time away from this project, I had received some functions from a member of the community (Universal in CodePlex), who had taken the time to work out some of the functions which are more server side related vs. actual manipulation of updates and clients. After taking some time to review the code and make a few adjustments (adding –WhatIf support where needed), I have added those functions into the latest version of PoshWSUS for your enjoyment!

New Functions:

  • Set-PoshWSUSConfigUpdateSource
  • Get-PoshWSUSConfigUpdateSource
  • Set-PoshWSUSConfigProxyServer
  • Get-PoshWSUSConfigProxyServer
  • Get-PoshWSUSConfigSupportedUpdateLanguages
  • Set-PoshWSUSConfigEnabledUpdateLanguages
  • Get-PoshWSUSConfigEnabledUpdateLanguages
  • Set-PoshWSUSConfigUpdateFiles
  • Set-PoshWSUSConfigSyncSchedule
  • Get-PoshWSUSConfigSyncSchedule
  • Set-PoshWSUSConfigTargetingMode
  • Get-PoshWSUSConfigSyncUpdateCategories
  • Get-PoshWSUSConfigSyncUpdateClassifications
  • Set-PoshWSUSConfigUpdateClassification
  • Set-PoshWSUSConfigProduct

Of course, while doing this I have realized that there is still a lot of work to be done here and have begun doing some more development on this to hopefully add a few more functions as well as tightening up the code on existing functions as well as taking the time to make the help a little more robust by removing the comment based help and work on the dedicated help files instead. Quite a bit of work ahead of me in doing just this stuff, but it will be worth it in the end.

Thanks for all of the support on this and I am looking forward to pushing out another update later on in the year!

Download PoshWSUS 2.2.1

Posted in Modules, powershell, WSUS | Tagged , , | 4 Comments

Using PowerShell Parameter Validation to Make Your Day Easier

Writing functions or scripts require a variety of parameters which have different requirements based on a number of items. It could require a collection, objects of a certain type or even a certain range of items that it should only accept.

The idea of parameter validation is that you can specify specific checks on a parameter that is being used on a function or script. If the value or collection that is passed to the parameter doesn’t meet the specified requirements, a terminating error is thrown and the execution of the code halts and gives you an error stating (usually readable) the reason for the halt. This is very powerful and allows you to have much tighter control over the input that is going into the function. You don’t want to have your script go crazy halfway into the code execution because the values sent to the parameter were completely off of the wall.

You can have multiple unique validations used on a single parameter and the style is similar to this:

[parameter(0]
[ValidateSomething()] #Not a legal type; just example
[string[]]$Parameter

Another important item is that you cannot use a default value in your parameter. Well, you can but it will never go through the parameter validation unless you specify a new value. While this will probably never apply or happen in your code, it is still something worth pointing out just in case you have something invalid that will not apply to whoever uses the function and wonders why it fails later in the code  rather than at the beginning.

I am going to go over some of the validation types and give examples of each as well as discuss potential issues with each approach.

[ValidateNotNullOrEmpty()] and [ValidateNotNull()]

I  have both of these listed instead of separately for a reason. Pick one or the other! I have seen some instances where both of these are being used to validate a single parameter and this simply does not need to happen and here is why:

  • ValidateNotNull only checks to see if the value being passed to the parameter is a null value. Will still work if it is passed an empty string.
  • ValidateNotNullorEmpty also checks to see if the value being passed is a null value and if it is an empty string or collection

Lets check out some examples with ValidateNotNull

Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateNotNull()] #No value
        $Item
    )
    Process {
        $Item
    }
}
# Will fail
Test-Something -Item $Null

image

# Will work because it is just an empty string, not a null value
Test-Something -Item ''

# Will work because we aren't checking for empty collections
Test-Something -Item @()

image

Notice that I didn’t specify a type for the parameter. If you specify a type, then this will not work properly. If you need to use a type for your parameter, then use ValidateNotNullOrEmpty instead.

Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateNotNull()]
        [string]$Item
    )
    $Item
}
 
# Will work
Test-Something -Item $Null
Test-Something -Item ''

image

Up next is ValidateNotNullOrEmpty which is great if you are using collections and/or require an object of a specific type.

Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()] 
        [string[]]$Item
    )
    Process {
        $Item
    }
}

It doesn’t matter if the parameter has a type with it, is a collection or is just a simple string, all of the below will fail when attempted.

Test-Something -Item $Null

image

Test-Something -Item @()

image

Test-Something -Item ''

image

As you can see, this does handle all of the possible empty and null values thrown at it. I will again reiterate that you need to choose one or the other with these two validations; no need to duplicate effort if you are just trying to avoid null values being passed into the parameter.

[ValidateLength()]

This is useful if you are expecting values of a certain length; such as usernames.

Some important issues to take note of include that will throw an error with the validation attribute:

  • Max length less than min length
  • Max length set to 0
  • Argument is NOT a string or integer
Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateLength(1,8)]
        [string]$Item
    )
    Process {
        $Item
    }
}

The first value given is the minimum value and the second value will always be the maximum value. In this case, I am expecting a string that is at least 1 character and at most 8 characters long. Anything outside of those boundaries will throw an error.

# Works
Test-Something -Item Boe

image

# Will fail
Test-Something -Item Thisisalongstring

image

Note that this tells you the length of the value that was submitted (17).

[ValidateRange()]

This is useful when you want to validate a specific range of integers, such as testing age.

Some important issues to take note of include that will throw an error with the validation attribute:

  • Value of MinRange is greater than MaxRange
  • Argument is NOT same type as Min and Max Range parameters
Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateRange(21,90)]
        [int[]]$Age
    )
    Process {
        $Age
    }
}
# Will work
Test-Something -Age 34

21,36 | Test-Something

image

# Will fail
Test-Something -Age 16

Test-Something -Age 100,25

25,115,21 | Test-Something

image

[ValidateCount()]

This is useful to keep only a certain number of values in a collection for a parameter.

Some important issues to take note of include that will throw an error with the validation attribute:

  • Value of MinRange is greater than MaxRange
  • Range types must be Int32
  • Parameter must be an array type ([string[]])
    • If you just use [string] (or similar), then you are bound to only 1 item to pass into the parameter
  • Min cannot be less than 0
Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateCount(1,4)]
        [string[]]$Item
    )
    Process {
        $Item
    }
}
# Will work
Test-Something -Item 9,10

image

# Will fail
Test-Something -Item 9,6,7,8,9

image

As you can see, it will tell you in the error how many items were being assigned to the parameter as well as how many items are allowed.

Note that it has little effect on items being passed through the pipeline (this is by design as it is how the pipeline is supposed to work).

1,2,5,8,10 | Test-Something

image

But what if we pass a collection of collections?

@(1,2),@(1,2,5,8,6),@(10,15,6)  | Test-Something

image

It will in fact fail on the collection that had more than the allotted items.

[ValidateSet()]

Useful limiting a certain set of item  and allows for case sensitive sets if using $False after defining set. Default value is $True (case insensitive).

[ValidateSet('Bob','Joe','Steve', ignorecase=$False)]

Some important issues to take note of include that will throw an error with the validation attribute:

  • Used more than once on a parameter (multiple sets of sets)
  • Element of set is in each element of an array being passed or fails completely
  • Parameter doesn’t accept array and more than 1 item passed
Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateSet('Bob','Joe','Steve')]
        [string[]]$Item
    )
    Process {
        $Item
    }
}
# Will work
Test-Something -Item 'joe'

image

@('Joe',@('Bob','Steve')) | Test-Something

image

# Will not work
Test-Something -Item 'Boe'
Test-Something -Item 'Boe','Joe'

image

#Partial; note how the collection of Bill and Joe doesn't work
@('Bob',@('Bill','Joe'),'Boe','Steve') | Test-Something

image

For more cool stuff you can do with ValidateSet, check out this article from Matt Graeber (Blog | Twitter): http://www.powershellmagazine.com/2013/12/09/secure-parameter-validation-in-powershell/

[ValidatePattern()]

Useful to validate input matches specific regex pattern allows for case sensitive matches; Regex Options flags allow for more customization.  Very hard to gather requirements from error message that is thrown if the validation fails unless the individual has some RegEx experience.

Member name

Description

Compiled

Specifies that the regular expression is compiled to an assembly. This yields faster execution but increases startup time. This value should not be assigned to the Options property when calling the CompileToAssembly method. 

CultureInvariant

Specifies that cultural differences in language is ignored. See Performing Culture-Insensitive Operations in the RegularExpressions Namespace for more information.

ECMAScript

Enables ECMAScript-compliant behavior for the expression. This value can be used only in conjunction with the IgnoreCase, Multiline, andCompiled values. The use of this value with any other values results in an exception.

ExplicitCapture

Specifies that the only valid captures are explicitly named or numbered groups of the form (?<name>…). This allows unnamed parentheses to act as noncapturing groups without the syntactic clumsiness of the expression (?:…).

IgnoreCase

Specifies case-insensitive matching.

IgnorePatternWhitespace

Eliminates unescaped white space from the pattern and enables comments marked with #. However, the IgnorePatternWhitespace value does not affect or eliminate white space in character classes. 

Multiline

Multiline mode. Changes the meaning of ^ and $ so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string.

None

Specifies that no options are set.

RightToLeft

Specifies that the search will be from right to left instead of from left to right.

Singleline

Specifies single-line mode. Changes the meaning of the dot (.) so it matches every character (instead of every character except \n).

 

Some important issues to take note of include that will throw an error with the validation attribute:

  • Used only once per parameter
  • Collection being passed must pass pattern for each item or fails completely if not coming from pipeline
Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')]
        [string[]]$Item
    )
    Process {
        $Item
    }
}
 

I am intentionally using a complex RegEx string for the IP address to prove a point on how difficult it could be to understand the error.

# Will work
Test-Something -Item 192.168.1.1
Test-Something -Item 192.168.1.1,168.125.12.15

image

# Will not work; note the error shows the regex, which only helps those that know regex
Test-Something -Item 'Joe'
Test-Something -Item 1
Test-Something -Item 192.168.1.1,23

image

As you can see, the error messages are pretty hard to read unless you know RegEx.

One last example showing input from the pipeline.

# Works a little better when using input from pipeline
@('192.168.1.1','23') | Test-Something

image

The error leads me to the last type of validation that we can use to make the error a little better.

[ValidateScript()]

Very powerful to test for various requirements and can do what others can do and provide better (custom) errors based on how you structure the code. Can also slow down your script execution if you have too many checks happening in the scriptblock or long running check.

Some important issues to take note of include that will throw an error with the validation attribute:

  • $True and $False values are not allowed to return when the attempt to validate fails or succeeds
    • I would highly recommend you not use the return value of $False and instead use Throw with a custom error message so the user knows what should be happening.
Function Test-Something {
    [cmdletbinding()]
    Param(
        [parameter(ValueFromPipeline)]
        [ValidateScript({If ($_ -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$') {
            $True
        } Else {
            Throw "$_ is not an IPV4 Address!"
        }})]
        [string[]]$Item
    )
    Process {
        $Item
    }
}

 

# Will work
Test-Something -Item 192.168.1.1
Test-Something -Item 192.168.1.1,168.14.12.15

image

Now we can see a better error message when this fails.

# Will not work
Test-Something -Item 'Joe'
Test-Something -Item 1
Test-Something -Item 192.168.1.1,23

image

Now instead of a RegEx error message, this actually tells you that it expects an IPV4 address instead. Same as the example below.

@('192.168.1.1','23') | Test-Something

image

My last example with this is to check for invalid characters in a given path.

Function Test-Something {
    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline)]
        [ValidateScript({
            If ((Split-Path $_ -Leaf).IndexOfAny([io.path]::GetInvalidFileNameChars()) -ge 0) {
                Throw "$(Split-Path $_ -Leaf) contains invalid characters!"
            } Else {$True}
        })]
        [string[]]$NewFile
    )
    Process {
        $NewFile
    }
}
#Works
Test-Something -NewFile "C:\Temp\File.txt"

image

#Fails
Test-Something -NewFile "C:\test\temp\File?.txt"

image

This was just one example of using ValidateScript, but you can pretty much test anything out and as long as you provide either a $True if good and Throw a custom error (or return $False), then you will have a pretty powerful method for validating parameters. I will reiterate again the need to keep this as efficient as possible as to not slow down your function if you are passing a large collection with each item being validated by your script block.

That’s it for working with parameter validation in PowerShell. Hopefully some of these examples and explanations will help you out in a future script/function!

Posted in powershell, Winter Scripting Games 2014 | Tagged , , , | 12 Comments