The first thing we do is set up the path we want to make. Then we test to see if the path exists. If they don’t, we make them. I’m using temp in this cause because I will be deploying this to 2000+ machines. We will remove the installer afterward. I want the Temp folder to existing afterward for future deployments.
Next, we grab the URL we want to work with This is the gimp’s official download portal. This portal is by default Oldest to newest when you pull from it using Powershell.
Then we use the Invoke-webrequest to grab the website as we did in a previous post. From there we grab all of the links. In this case, since it’s a repo, they are all download links except for 2. We only want the exes of the list, so we use a where-object to find those. Then we select the last 1 as it is the newest version.
Now we need to build our URL and our Path. This is some string controls. Notice the $($Something.Something) in this code. When you deal with an array in a string and want to grab a sub item, you need to call it out with the $().
Now we want to uninstall the pervious version of Gimp. Since gimp doesn’t show up in the win32_products, we go to it manually in the file system. Newer gimps host themselves inside the program files > gimp 2. So we search to see if that folder exists with a test-path. If it does, we then check to see if gimp is running. Then kill it with fire… ok, not fire, but force. Gimp is awesome about putting an uninstaller inside the file system. So we will use that. It’s located in the Gimp 2 > Uninst > Unins000.exe. Which can be triggered with a /verysilent parameter to keep it quiet. We do this with a start process and we use a flag -wait to wait on it to uninstall.
Then we start the install of the new gimp with the start-process again. We use the Download Name we made eailer with an argument list of /verysilent /norestart /allusers and a -wait.
Here is a little powerhouse script I wrote to audit the mailbox sizes. The focus recently is to see who’s mailbox sizes are about to be over and if it’s the deleted folder. I can’t show you all of the scripts, but I can show you part of it. This part will pull the data down in such a way that you can view the mailbox sizes, who has the largest mailboxes, what folder is the largest in those mailboxes, and what their deleted folder is sitting at. Let us take a look at the script itself.
The Script
function Get-SHDEXOMailboxSizeAudit {
[cmdletbinding()]
param (
[pscredential]$Credential
)
Begin {
#Installs required modules
Write-Verbose "Installing required modules"
if (!(Get-InstallEdModule ExchangeOnlineManagement)) { Install-Module ExchangeOnlineManagement }
Write-Verbose "Checking and importing required modules"
# Starts importanting required modules
if (!(Get-Command Connect-ExchangeOnline)) { Import-Module ExchangeOnlineManagement }
}
process {
#Connecting to exchange. Grabing credentials.
if (!($PSBoundParameters.ContainsKey('Credential'))) {
$Credential = (Get-Credential)
}
Connect-ExchangeOnline -credential $Credential
#Grabs all the mailboxes at once. As this report will look at all the mailboxes.
$Mailboxes = Get-Mailbox
#Starts looping through each mailbox
For ($M = 0; $M -le $Mailboxes.count; $M++) {
Write-Verbose "Gathering Info on: $($Mailboxes[$M].UserPrincipalName)"
#Grabs the mailbox stats.
$Stats = Get-EXOMailboxStatistics $mailboxes[$M].UserPrincipalName
$FolderStats = Get-EXOMailboxFolderStatistics $mailboxes[$M].UserPrincipalName
#Starts looping through those folders to get their sizes.
$MainFolderStats = foreach ($Folder in $FolderStats) {
$FolderSize = [math]::Round((($Folder.FolderSize.split('(')[1].split(' ')[0] -replace ',', '') / 1gb), 2)
[pscustomobject]@{
Name = $Folder.name
Size = $FolderSize
}
}
#Adds this information to the mailbox object
$Mailboxes[$M] | add-member -Name "Statistics" -MemberType NoteProperty -Value $Stats
$Mailboxes[$M] | add-member -Name "FolderStats" -MemberType NoteProperty -Value $MainFolderStats
}
#Creates a return value.
$Return = foreach ($Mail in $Mailboxes) {
#Starts looking at mailboxes that are not the discovery mailbox.
if (!($mail.UserPrincipalName -like "DiscoverySearchMailbox*")) {
#Grabs the deleted folder as that is a folder we want to see in this report.
$Deleted = $Mail.FolderStats | where-object { $_.name -like "Deleted Items" }
#Grabs the largest folder. If it's not the deleted folder, then we might want to increase their mailbox sizes.
$LargestFolder = $Mail.FolderStats | Sort-Object Size -Descending | select-object -first 1
#Doing some math on a string. The string format (# bytes). Thus, we work the string to get the bytes. Divide and round.
$Size = [math]::Round(($Mail.Statistics.TotalItemSize.value.tostring().split('(')[1].split(' ')[0].replace(',', '') / 1gb), 2)
#Grabs the mailboxes percentage.
$DeletedToMailboxPercent = [math]::Round((($Deleted.Size / $size) * 100), 0)
#Outputs the data to the return value.
[pscustomobject]@{
DisplayName = $Mail.Displayname
UserPrincipalName = $Mail.UserPrincipalName
MailboxSize = $Size
LargetsFolder = $LargestFolder.Name
LargetsFolderSize = $LargestFolder.Size
DeletedItemSize = $Deleted.Size
DeletedToMailboxPercent = $DeletedToMailboxPercent
}
}
}
#Disconnects exchange
Disconnect-ExchangeOnline -confirm:$false > $null
}
End {
$Return | sort-object MailboxSize -Descending
}
}
The breakdown
First this script is designed to work on powershell 7. It will not work on powershell 5.
The first part we come to is the [pscredential] object. Notice it’s not mandatory. Notice no pipelining either. I have found PS credentials pipped intend to do very poorly. So, it’s simple, bam wam done.
Begin
Inside our begin tab, we have the module setup. We check installed modules for exchangeonlinemanagement. if it’s there, we ignore it and import the module, if it’s not we install it. Same way with importing. If it’s imported, then we do nothing, if it’s not, we import it.
Process
Next, we grab credentials if need be and connect to exchange. We use the get-credential command to grab the credentials. Then we use the connect-exchangeonline command to connect to the exchange online.
Once we are connected, the magic can start. This is the sweetness of this script. The first step is to grab all of the mailboxes at once with Get-Mailbox. Then we start a loop, not any loop, a for a loop. Wait! David, WHY A FOR LOOP! It’s simple, we want to add information to the index. So, we need to be able to call the index. We use the Get-EXOMailboxStatistics and choose the userprincipalname of the index we are looking for. We do the same thing with Get-EXOMailboxFolderStatistics. These gives us some clear stats we can use later on in the script. Now we loop through the folder stats that we just collected and math ourselves some bytes to gb. See the output of the get-exomailboxfolderstatistics looks like “24mb (#### bytes). So we need to filter that out and get the ####. I use a split to do this. I split out the string at the ‘(‘. This way, the bytes are on object 1. Then we split it again by the space. Now the bytes are on the 0 object. Then we replace the ‘,’ with nothing. Now we divide all that by 1gb to convert it to gbs. Then we drop that all into the mainfolderstats. Next, we add all of that information into the mailbox variable we created before this looping madness using add-member. We are adding it as a noteproperty.
Now we have prepped our data, it’s time to sort it. We start the loop once more, but this time it’s simple for each loop as we don’t need the array index value. We first grab all the deleted items folder. Then we grab the largest folder using the sort-object command on the size object of the folderstats note property that we made in the last step. Then we do some math. Like before to get the mailbox overall size. Finally, we grab the deleted mailbox percentage with a little more math. This time its percentage math. Now we have all of this useful information we use our pscustomobject and put it all together.
Then we disconnect using the disconnect-exchangeonline command.
End
Finally we display the return information inside our end tab. We sort the object by the mailbox size in a descending order.
This past week I needed to find all of the quickbook files on a computer without accessing quickbooks itself. The core of the script is a simple Get-Childitem command looped for each logical disk on the machine. Looking for one of the four main extensions for quickbooks. The four extensions are as follows
Since I want this to be a little easier to use, I broke it down between the file types with a validate set parameter. This way you can choose which extension you want. Then I go through each extension and make an if statement for each one matching up the extension.
Next we get the disks using the Get-CimInstance -classname win32_logicaldisk. This grabs all the mapped drives, local drives, and anything else that has a drive letter.
Now we loop through those disks and search the root of each drive for any files with the extension we choose. We select the fullname as this gives us the full path. I also like having file size and last write time to determine if the file is valid still. Once we go through this loop we display the information.
Improvements
I can add remote computers to this setup.
That’s it, if you have any questions feel free too reach out.
This little guy is a simple dynamic parameter resource for you all. Take a look at my previous blog post about how these parameters work. Totally worth adding these things to your scripts. This script is simple, it uses the add-adgroupmemeber and dynamic parameters to help pad against mistakes.
The Script
function Set-SHDADGroupMemebers {
[cmdletbinding()]
param (
[parameter(mandatory=$true)][validateset('Add','Remove')][string]$Action,
[securestring]$Credential,
[switch]$Output
)
DynamicParam {
# Set the dynamic parameters' name
$ParamName_portgroup = 'Group'
# 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)
# Create the dictionary
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Generate and set the ValidateSet
$arrSet = (Get-ADGroup -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_portgroup, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParamName_portgroup, $RuntimeParameter)
# Set the dynamic parameters' name
$ParamName_datastore = 'Username'
# 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_datastore, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParamName_datastore, $RuntimeParameter)
return $RuntimeParameterDictionary
}
begin{
$Group = $PsBoundParameters[$ParamName_portgroup]
$username = $PsBoundParameters[$ParamName_datastore]
}
process {
if ($PSBoundParameters.ContainsKey('Credential')) {
if ($Action -like "Add") {
Add-ADGroupMember -Identity $group -Members $username -Credential $Credential
} elseif ($Action -like "Remove") {
Remove-ADGroupMember -Identity $group -Members $username -Credential $Credential
} else {
Get-ADGroupMember -Identity $group -Credential $Credential
}
} else {
if ($Action -like "Add") {
Add-ADGroupMember -Identity $group -Members $username
} elseif ($Action -like "Remove") {
Remove-ADGroupMember -Identity $group -Members $username
} else {
Get-ADGroupMember -Identity $group
}
}
}
end {
if ($Output) {
if ($PSBoundParameters.ContainsKey('Credential')) {
Get-ADGroupMember -Identity $Group -Credential $Credential
} else {
Get-ADGroupMember -Identity $group
}
}
}
}
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.
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.
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.
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.
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.
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.