Multiple Dynamic Parameters

Multiple Dynamic Parameters

Dynamic parameters… OMG… Lets just say, when I first started using them, my brain melted. These things take so much effort to get going. Then the way to document them is even harder. After using PowerShell for many years, I finally figured them out and it’s time to help you out. There are many blogs out there and many posts on redit about Dynamic Paramters. All of them fall short one way or another. I am going to try and not do that today. By the end of this blog we will create a Add-SHDUserToGroup command with Two Dynamic Parameters of User and Group.

Structure

So, Dynamic Parameters is a parameter that is more fluid. You can pull the structure of Active Directory and tab through users as a parameter or you can look at the inside of a file. The limits are bound to a single column array. This means you can’t use all of the Get-ADuser command. You have to parse it out with something like select-object. We will get more into that later. First thing first. Structure. Where does a Dynamic Parameter start? It starts after the param() and before your Begin {} tag.

function Add-SHDUserToGroup {
    [cmdletbinding()]
    param ()
    DynamicParam {}
    Begin {}
    Process {}
    End {}
}

To use the Dynamic Parameters you have to have the param() tag. This tag can stay blank. As scripters we always want our functions to be advanced functions. This way we can see the verbose tags as we run our tests. Thus me, you will need to run an test more than once.

Planning Stage

Before you go any farther. You need to plan out your Dynamic Parameter. In our example, I will be working with a company that has over active 4000 users in their AD. So, a get-aduser -filter * would bring running the command to it’s knees. The object you target to gather information from will execute a command and it will effect the PS session you are in. It may even crash your powershell. I learned this the hard way when calling a script to gather every user’s name from Azure AD. I always suggest using the # and list out the commands you want to run. Study those commands and find the fastest approach. You don’t want your user waiting 5 minutes to hit tab or spend 5 minutes tabbing through options they don’t need. The name should also be reflective. In this example we will be using one a single object and not an array. So no string[].

The Setup

Lets go line by line. I found the original part of this bad boy online on github. However, where it failed was how to use it. Then I found a script that showed how to use the dynamic parameter. I was confussed, but Hopefully you will not be. The first thing we want to do is name it.

$ParamName = "User"

The next item is to create a collection of attributes. The first part of this command creates the array. Kind of like an $a =@() deal, but not. We are going to use the System > Collections > Object Model > collections and select the system attributes. Inside the object Model there are many other types of collection items. Avoid those for now.

$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

Now we need to create and set the parameter’s attributes. We create the attribute set with the new-object cmdlet like before. System > Management > Automation > ParameterAttribute. This is the life of the parameter’s attribute. Then we set the attribute to mandatory by adding the .Mandatory = $true flag. Then we finally add this to our AtrributeCollection object we made in the previous steps using the .Add() method.

$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute) 

Now it’s time to create the Directory. Once again, this is an object like before, thus new-object. We are living inside the System > Management > Automation world. This time we want the RuntimeDefinedParameterDictionary.

$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

The dirctionary has been created. Now we need to fill that dictionary up with information to select from. So, lets create an array object and fill it it up. Then we will place that object into the dictionary. This is also the important part because this is the “Dynamic” part of the dynamic parameters. We will be selecting all the enabled user. Then we create a new object for the validation set. Similar to the [validateset()] inside the parameters. Then we add the vlidateset to the attributecollection again using the .add() option.

$arrSet = (Get-ADuser -Filter {enabled -eq $true}).name
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
$AttributeCollection.Add($ValidateSetAttribute)

Now we need the run time parameter set. We do this using the new-object. This set will basically hold the object’s name, its type, and the collection. Imagine it as the [validateset(“A”,”b”,”c”)][string]$users. Once again System > management > Automation > RuntimeDefinedParameter. Name, type, and collection. Once we have made this with the previous information, we need to add it to our dictionary with an add() once again.

$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParamName_portgroup, $RuntimeParameter)

Finally we want to return the RunTimeParameterDictionary.

return $RuntimeParameterDictionary

Phew, that’s a lot for one parameter! However, now we have a parameter that gives us a userful list of users with their system names we can call from. How do we use that information?

Begin Tag

Inside the begin tag, lets put the parameter into something that we can use inside the process tag.

$UserName = $PsBoundParameters[$ParamName]

Process tag

Now in the process tag all we have to do is use the information above ($username) and use that instead of using the same old information over and over again. We search for the username of the user.

if ($PSBoundParameters.ContainsKey('Credential')) {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        } else {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        }

End Tag

We finally release the data with the $returnInfo at the end.

end {
     $ReturnInfo
}

The Script

function Get-SHDADUsername {
    [cmdletbinding()]
    param (
        [securestring]$Credential
    )
    DynamicParam {
               
        # Set the dynamic parameters' name
        $ParamName = 'Users'
        # Create the collection of attributes
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)  
        # Generate and set the ValidateSet 
        $arrSet = (Get-ADUser -Filter *).name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)
        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParamName, $RuntimeParameter)
        return $RuntimeParameterDictionary
    }

    begin{
        
        $username = $PsBoundParameters[$ParamName] 
    }
    process {
        if ($PSBoundParameters.ContainsKey('Credential')) {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        } else {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        }
    }
    end {$ReturnInfo}
}
Message in The Ballon

Message in The Ballon

Do you need something to let your end-user know what’s going on? Don’t want your end-user to see the program running? Well, PowerShell has the ability to use the system windows notify system. AKA, the message box that pops up on the right of the screen.

The first step to make this happen is we need to add the assembly, system.windows.forms. We do this by the amazing command add-type.

add-Type -AssemblyName System.Windows.Forms

Now we have the assemblies loaded into ram, we need to create an object from those assemblies. This way we can command the object later. We do this using the New-object cmdlet. We want the notification tray icons. So we want the system.windows.forms.notifyicon.

$global:balloon = New-Object System.Windows.Forms.NotifyIcon

You know that notifications have icons. Yep, It’s from the application itself. So, if you have this function in another application, it will show that the application’s icon. We call upon the System.Drawing.Icon to do this. But First, we need to know which app we are currently using. We do this with Get-Process with the ID and the current Pid.

$path = (Get-Process -id $pid).Path
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path) 

What’s cool about this is we are extracting the icon of the application itself with that extract associated icon. We push it into our notify icon object. Next, we want to add the Tip Icon. That’s the ! ? so on and so forth. We do this by using the system.windows.forms.tooltipicon function. Believe it or not, we can use a validate set in the parameters to get the exact naming input as well. The property of the object is BalloonTipIcon.

$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::$Messagetype

Now the message itself. At this point we have an icon, the notification icon and that’s it. This is the meat of the notification. The propriety in the balloon object is, BalloonTipText.

 $balloon.BalloonTipText = $Message

Just a quick note, to much text is ugly.

Next is the title, I like to use just the word attention as it brings the users eyes to it. The balloon’s propriety for this is BalloonTipTitle.

$balloon.BalloonTipTitle = "Attention"

Finally, we want to make this thing visible. An invisible tooltip is as useful as invisible ink without a black light. As you have guessed by now, Visible is the balloon’s propriety.

$balloon.Visible = $true 

Now the drum roll, please! It’s time to show our masterpiece that will be enjoyed by the end-user for a split second. The ShowBalloonTip(). Yes, let us do it! This is a method and will show the balloon tip for x time. This is set up in milliseconds. So, every 1000 milliseconds makes a second.

$balloon.ShowBalloonTip($Time)

Lets put it all together shall we. Behold the script of the balloon tip.

The Script

Function Invoke-SHDBallonTip {
    [cmdletbinding()]
    param (
        [string]$Message = "Your task is finished",
        [int]$Time = 10000,
        [validateset("Info", "Warning", "Error")][string]$Messagetype = "Info"
    )

    #Adds the reqruired assembly's for the ballon tips. 
    Add-Type -AssemblyName System.Windows.Forms 

    #Creates the notifiy icon that will appear in the notification tray. 
    $global:balloon = New-Object System.Windows.Forms.NotifyIcon
    $path = (Get-Process -id $pid).Path
    $balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path) 
    $balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::$Messagetype
    $balloon.BalloonTipText = $Message
    $balloon.BalloonTipTitle = "Attention" 
    $balloon.Visible = $true 
    $balloon.ShowBalloonTip($Time)
}

That’s it, folks. Such a simple thing. Here are a few things you can do. You can change the $path to $Path = Get-Process -id $PID. Then in the balloon.icon, use $path.path. In the balloon tip title, you can put $path.description and now you have the description as the title. Other than that, it’s pretty straight forward.

I would love to hear back from yall. Let me know if there is anything you would like for me to write about.

Message in a bottle – The Box

Message in a bottle – The Box

This will seem dumb, but do you need a message box in your script? Normally no, but when you are using a script for an end-user, you might need it. So, let us take a look at how to do it real quick.

The Script

function Invoke-SHDMessageBox {
    [cmdletbinding()]
    param (
        [Parameter(HelpMessage = "Message Title", Mandatory = $true)][Alias('Title', 'header')][string]$MessageTitle,
        [Parameter(HelpMessage = "Message Body", Mandatory = $true)][Alias('Boby', 'Information')][string]$MessageBody,
        [Parameter(HelpMessage = "Message Buttons", Mandatory = $true)][Alias('Buttons')][validateset("YesNo", "YesNoCancel", "OK", "OKCancel")][string]$MessageButtons,
        [Parameter(HelpMessage = "Message Image", Mandatory = $true)][Alias('Image')][validateset("Asterisk", "Error", "Exclamation", "Hand", "Information", "None", "Question", "Stop", "Warning")][string]$Messagetype
    )
    Add-Type -AssemblyName PresentationCore, PresentationFramework
    $ButtonType = [System.Windows.MessageBoxButton]::$MessageButtons
    $MessageIcon = [System.Windows.MessageBoxImage]::$Messagetype
    $Results = [System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $messageicon)
    Return $Results
}

The Breakdown

Lets break this guy down. First thing first, the parameters.

  • MessageTitle – What will be presented as the title of the message box.
  • MessageBody – The message itself.
  • MessageButtons – What types of buttons.
    • YesNo
    • YesNoCancel
    • OK
    • OKCancel
  • MessageType – The type of image will be presented.

All the parameters are mandatory. None can be passed by the pipeline. All are single strings instead of my normal list of strings. It’s far easier that way.

Next, we need to add the assembly for the box. These are PresentationCore and PresentationFramework.

Add-Type -AssemblyName PresentationCore, PresentationFramework

Next, we just add items to the variables we will be using. The input from the parameters and their validate set. For the buttons themselves we call upon the power of the assembly we added earlier. [System.Windows.*] In the button case, the * becomes MessageBoxButton.

    $ButtonType = [System.Windows.MessageBoxButton]::$MessageButtons

Then the message box image will be the same way, but with messageboximage.

$MessageIcon = [System.Windows.MessageBoxImage]::$Messagetype

Then we put it all together with the messagebox itself.

$Results = [System.Windows.MessageBox]::Show($MessageBody, $MessageTitle, $ButtonType, $messageicon)

The first part of the show is the body followed by the title, then followed by the buttons, and finally the image. Notice I pipe it into Results. I do this because we want to return those results.

Return $Results

That’s it! Once you return the results, you can test them with an if statement. Come back Monday for a ballon notification function.

Convert a Mac Address to a CIDR with Powershell

Convert a Mac Address to a CIDR with Powershell

Today, I needed to convert a mac address to a CIDR. The lesson I learned about this is to go back to the basics instead of looking around for a quick answer. So, to the basics first.

A mac address is a 4 octet number that represents a binary position. It ranges between 1 and 255. This means something like 255.255.255.0 is represented in binary as: 1111 1111 1111 1111 1111 1111 0000 0000

It’s easier to remember the 255 number instead of all those ones and zeros. However, what’s that CIDR? Well, what’s a CIDR? A CIDR represents the mac address and how many positions there are. If you count the number above, it turns into 24 1s and 8 0s. Thus the CIDR is 24. Very simple.

Now, how do you change 240.0.0.0 to a cidr? You can go to a chart and look or we can use PowerShell. The first thing we need to do is split up this string.

"240.0.0.0" -split '\.' 

We split the string up with a split command and tell it to split it up by the . item. We use an exit \ because the . is a special character for regex. Next, we loop through this list of octets with a foreach-object loop.

$Mask -split '\.' | ForEach-Object {
        
    }

Then we break down each octet into a binary number. We do this with a system.convert and tell it the number we bytes we want which is 2.

[System.Convert]::ToString($_, 2)

We then pad that information because sometimes 0s will produce more useless items.

([System.Convert]::ToString($_, 2).PadLeft(8, '0'))

The code above will give us the 1s and 0s of the binary number. Now we need to count the ones. We do this by converting the string to a char array with at .toochararray(). We search the string with a where-object for the number 1 and finally count it.

(([System.Convert]::ToString($_, 2).PadLeft(8, '0')).tochararray() | where-object { $_ -eq '1' } | measure-object).count

Now we need to add each item up. We do this by adding a variable before the loop set to 0 and then add the variable to the variable and the count. Finally, after the loop, display the information.

$Cidr = 0
    $Mask -split '\.' | ForEach-Object {
        $Cidr = $Cidr + (([System.Convert]::ToString($_, 2).PadLeft(8, '0')).tochararray() | where-object { $_ -eq '1' } | measure-object).count
    }
    $Cidr

That’s it yall! We count the number of 1s. That will give us our cidr count.

Script

Have fun with the script!

function ConvertFrom-SHDIPv4MaskToCidr {
    param (
        [string]$Mask
    )
    $Cidr = 0
    $Mask -split '\.' | ForEach-Object {
        $Cidr = $Cidr + (([System.Convert]::ToString($_, 2).PadLeft(8, '0')).tochararray() | where-object { $_ -eq '1' } | measure-object).count
    }
    $Cidr
}
Invoke-SHDMoveFiles

Invoke-SHDMoveFiles

Ever need a service that copies a single folder to multiple locations at once? This script will do just that. It will copy a single location to more than one location and even log the outcomes accordingly. Thus, you will be able to set this one up as a task and run it every so often.

Invoke-SHDMoveFiles {
    <#
    .SYNOPSIS
        Copy files from target source folders to a set of target folders.
    .DESCRIPTION
        Copys from multiple source folders to a select target folders. This allows archiving to occur.
        The folders are not auto created. Thus, the folders must exists. The script also adds a log file of any error
        messages you see. See notes for more details.
    .PARAMETER SourceFolder
        [String[]] SourceFolder is an Array of Strings of target folders to move the files from.
    .PARAMETER TargetFolder
        [String[]] TargetFolder is an Array of strings where the files will be copied to. This is not a one for one ratio. Thus all files will exist inside the targetfolder.
    .EXAMPLE
        ./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder\','C:\tbc\tmp\Source Folder 2\' -TargetFolder 'C:\tbc\tmp\Target Folder\','C:\tbc\tmp\Target Folder 2\'
        Moves all files from the source folder 1 and 2 to target folders 1 and 2.
    .EXAMPLE
        ./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder\' -TargetFolder 'C:\tbc\tmp\Target Folder\','C:\tbc\tmp\Target Folder 2\'
        Moves all files from the source folder 1 to target folders 1 and 2.
    .EXAMPLE
        ./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder 1\','C:\tbc\Tmp\Source Folder 2\','C:\tbc\Tmp\Source Folder 3\'  -TargetFolder 'C:\tbc\tmp\Target Folder\'
        Moves all files from the source folder 1, 2, and 3 to the target folder
    .EXAMPLE
        ./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder\','C:\tbc\tmp\Source Folder 2\' -TargetFolder 'C:\tbc\tmp\Target Folder\','C:\tbc\tmp\Target Folder 2\' -Recurse
        Moves all files under Source folder 1 and 2 to target folders 1 and 2.
    .EXAMPLE
        ./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder\' -TargetFolder 'C:\tbc\tmp\Target Folder\','C:\tbc\tmp\Target Folder 2\' -Recurse
        Moves all files under source folder 1 to target folders 1 and 2.
    .EXAMPLE
        ./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder 1\','C:\tbc\Tmp\Source Folder 2\','C:\tbc\Tmp\Source Folder 3\'  -TargetFolder 'C:\tbc\tmp\Target Folder\' -Recurse
        Moves all files under source folder 1, 2, and 3 to the target folder
    .INPUTS
        [String] Folder Paths
    .OUTPUTS
        No Output
    .NOTES
        Author: David Bolding
        Date: 8/16/2020
        Purpose: Moving files around and making archives
        Min Powershell Version: 4

        This command generates error logs located c:\tbc\SHDMoveFileLog.txt. The error logs structure is broken up in 4 sections seperated with by ;
        1;$((Get-Date).tostring());Catch;$($_.Exception)
        1 - The placement of the error physically inside the script.
        2 - The date and time of the error
        3 - What type of error
        4 - Information about the error

        Run this command inside a scheduled task every 5 minutes to copy files needed to required locations. Make sure the
        tasks running user has full access right to the file locations provided.
    .LINK
        https://bolding.us
    #>
    [cmdletbinding()]
    param (
        [Parameter(HelpMessage = "Source Folder", Mandatory = $True)][string[]]$SourceFolder,
        [Parameter(HelpMessage = "Target Folders", Mandatory = $True)][string[]]$TargetFolder,
        [parameter(HelpMessage = "Recurse the Source Folder")][switch]$Recruse
        [parameter(HelpMessage = "Log Location")][string]$Logs = "C:\tmp\SHDMoveFileLog.txt"
    )
    #Tests to see if local c:\tbc exists, if it doesn't, it will create the path.
    if (Test-Path (Split-Path $logs)) { "" >> $logs } else { mkdir (Split-Path $logs) }

    #Start with the first source
    foreach ($Source in $SourceFolder) {

        #Tests to see if the source path exists.
        if (Test-Path -Path $Source) {

            #Grabs the required files from the first source path. It only grabs files.
            #Also checks if there is a recruse flag. If so, recurses accordingly.
            if ($Recruse) { $files = Get-childitem -Path $Source -File -Recurse } else { $files = Get-childitem -Path $Source -File }

            #Next we sort through the files in question
            foreach ($File in $files) {

                #Create a test bool.
                $success = $false

                #Starts the target folder sorting
                foreach ($Target in $TargetFolder) {

                    #Tests to see if target folder exists. If not, logs.
                    if (Test-Path -Path $target) {

                        #Enter a try catch to copy the files
                        try {

                            #Copys a single file to target folder. Overwrites any other there without confirmation
                            #No Confiramtion due to the lack of human Interaction.
                            Copy-Item -Path $file.fullname -Destination $Target -Force -Confirm:$false

                            #If no error so far, sets the success bool to true
                            $success = $true
                        }
                        catch {

                            #a failure occured, thus we set the success bool to false
                            $success = $false

                            #We log the error. This is the first log that shows up in the system.
                            #We date it.
                            #We state it's a catch
                            #Then we give the reason the try catch gives.
                            "1;$((Get-Date).tostring());Catch;$($_.Exception)" >> $logs
                        }
                    }
                    else {
                        #We log the fact that we can't reach the target location
                        "2;$((Get-Date).tostring());CanNotFind;$Target" >> $logs
                    }
                }
                #We test the bool for success.
                if ($success -eq $true) {
                    try {

                        #if successful we remove the file.
                        Remove-Item -Path $file.FullName -Force -Confirm:$false
                    }
                    catch {

                        #If we can't remove the file we log the reason why.
                        "3;$((Get-Date).tostring());Catch;$($_.Exception)" >> $logs
                    }
                }
            }
        }
        else {
            #We log the fact we can't reach the source location.
            "4;$((Get-Date).tostring());CanNotFind;$Source" >> $logs
        }
    }
}

Examples

./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder\' -TargetFolder 'C:\tbc\tmp\Target Folder\','C:\tbc\tmp\Target Folder 2\'

Moves all files from the source folder 1 to target folders 1 and 2. This is good for a backup of files. What I use this for is a custom software at work. It other products drop to location 1. Then it has a backup folder and the system folder. There we go, good as gold.

./Invoke-SHDMoveFiles -SourceFolder 'C:\tbc\Tmp\Source Folder 1\','C:\tbc\Tmp\Source Folder 2\','C:\tbc\Tmp\Source Folder 3\'  -TargetFolder 'C:\tbc\tmp\Target Folder\' -Logs c:\tmp\Combiner.txt

Moves all files from the source folder 1, 2, and 3 to the target folder. It also logs any errors. This is useful when trying to come folders together.

Notes

  • This script was designed to be ran as a task. If it doesn’t grab a file the first time around, it will grab it the second time around. It does this by using the try catch. It trys to move a file. if it doesn’t, it logs it and leaves it. We do that because a lot of times these files are being created and have a lock state. It’s best not to move them during that state.
  • This script auto logs into the c:\tmp location if you don’t state otherwise when things go wrong.