Embed Files into Powershell Scripts

Embed Files into Powershell Scripts

Do you have a zip file or an msi file that you need to embed into a powershell script for deployment. This is a quick and simple way to do this. The method is pretty sound and works most of the time. The larger the file, the more troubles the system has converting and converting back, so small files works wonders for this method.

The first thing we are going to do is convert a file into a base 64 string. This is done through the command Get-Content and the system convert method. Powershell 7 recently changed the command for this method and we will cover that first.

Powershell 7 Command

The get content changed the encoding flag to asbytestream flag.

$Content = Get-Content -Path $Path -AsByteStream -raw

This command will read the data as raw byte data which is needed for the System convert method. The Powershell 5 method users Encoding and then you select your type which is byte.

Powershell 5

$Content = Get-Content -Path $Path -Encoding Byte

These commands reads the data as raw information that will be used for the system.convert to base 64 string method.

$base64 = [system.convert]::ToBase64String($content)

Now the Base64 contains something that looks like this:

"//4wADEAMAAwADAAMAAwADAAZAAwADgAYwA5AGQAZABmADAAMQAxADUAZAAxADEAMQA4AGMANwBhADAAMABjADAANABmAGMAMgA5ADcAZQBiADAAMQAwADAAMAAwADAAMABlADYAYQAyADMAOAA4ADIAOQA3ADIAOQBiADIANABhAGEAMAA0ADcAMQBjADEANQBjADcAYwAwAGQANQA4AGYAMAAwADAAMAAwADAAMAAwADAAMgAwADAAMAAwADAAMAAwADAAMAAwADAAMwA2ADYAMAAwADAAMABjADAAMAAwADAAMAAwADAAMQAwADAAMAAwADAAMAAwAGIAMQA5ADcAOAA5AGQAYgBmADkAMAAxADYANgA2ADEANQBlADgAMwAzAGQAOQAxADMANgA0ADEAYwBmAGIANAAwADAAMAAwADAAMAAwADAAMAA0ADgAMAAwADAAMAAwAGEAMAAw==

We take that information and Pipe it into the clip.exe to bring it to our clipboard.

$Base64 | clip.exe

Converting it back

Now we need to convert the string back to a file. To do that we are going to use the System.Convert from base 64 string.

$Object = [System.Convert]::FromBase64String("The crazy text")

Now we have an object of random-looking data like before. We need to write those bytes to a file we do that with the System.IO.File write all bytes method. We will need the outfile location. and the object we created earlier to complete this task.

[system.io.file]::WriteAllBytes($OutFile,$object)

This will create the file you embedded into your powershell script.

Use Case

Encrypted Passwords

Let’s secure a password file and key file inside our embedded script. First, we are going to create the key for out password file.

$key = New-Object byte[] 16
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
$key | Out-File C:\temp\TestPassword.key 

This is the content of our key file:

41
202
16
223
129
18
138
116
106
132
9
217
180
41
226
108

Now we have our key made it’s time to create the password file.

$Password = "lazyAppl351" | ConvertTo-SecureString -AsPlainText -Force 
$Password | ConvertFrom-SecureString -Key $Key | Out-File c:\temp\TestPassword.txt

This is what the Password file looks like on the inside:

76492d1116743f0423413b16050a5345MgB8ADkAMAA1AGYAaABaAC8ASAB3AG8AUQBQAGUAdwBKAHgAbwBJAFIAKwA5AHcAPQA9AHwAZQA2ADQAOQBiADMAMQA2AGEAYwBjADAANAA4AGUANQAxAGMAOAA2ADgAOABkADkAYgBmAGIANgBkAGUAMgAwADQAZQA3AGQAMgA2ADMAMQBiADQANQA4AGIAOAA1ADcAMABlADAAMQBhAGIAYgA4AGIAYQBhADEAOAA4ADMANAA=

Now we will embed these files into our script.

$KeyFile = Get-Content -Path C:\temp\TestPassword.key -Encoding Byte
$base64 = [system.convert]::ToBase64String($Keyfile)
$Base64 | clip.exe

This will convert the key file into a working string that we can convert later. This is what the information looks like:

NDENCjIwMg0KMTYNCjIyMw0KMTI5DQoxOA0KMTM4DQoxMTYNCjEwNg0KMTMyDQo5DQoyMTcNCjE4MA0KNDENCjIyNg0KMTA4DQo=

Now we will do the same thing for the password file.

$KeyFile = Get-Content -Path C:\temp\TestPassword.txt -Encoding Byte
$base64 = [system.convert]::ToBase64String($Keyfile)
$Base64 | clip.exe

This is what this looks like:

NzY0OTJkMTExNjc0M2YwNDIzNDEzYjE2MDUwYTUzNDVNZ0I4QURrQU1BQTFBR1lBYUFCYUFDOEFTQUIzQUc4QVVRQlFBR1VBZHdCS0FIZ0Fid0JKQUZJQUt3QTVBSGNBUFFBOUFId0FaUUEyQURRQU9RQmlBRE1BTVFBMkFHRUFZd0JqQURBQU5BQTRBR1VBTlFBeEFHTUFPQUEyQURnQU9BQmtBRGtBWWdCbUFHSUFOZ0JrQUdVQU1nQXdBRFFBWlFBM0FHUUFNZ0EyQURNQU1RQmlBRFFBTlFBNEFHSUFPQUExQURjQU1BQmxBREFBTVFCaEFHSUFZZ0E0QUdJQVlRQmhBREVBT0FBNEFETUFOQUE9DQo=

Now we build the script with these two pieces of information. We recreate the files from the script and then pull them in like before. Here is the key code:

$keyBase64 = "NDENCjIwMg0KMTYNCjIyMw0KMTI5DQoxOA0KMTM4DQoxMTYNCjEwNg0KMTMyDQo5DQoyMTcNCjE4MA0KNDENCjIyNg0KMTA4DQo="
$Object = [System.Convert]::FromBase64String($keyBase64)
[system.io.file]::WriteAllBytes('C:\temp\TestPassword.key',$object)

and here is the Password code:

$PassBase64 = "NzY0OTJkMTExNjc0M2YwNDIzNDEzYjE2MDUwYTUzNDVNZ0I4QURrQU1BQTFBR1lBYUFCYUFDOEFTQUIzQUc4QVVRQlFBR1VBZHdCS0FIZ0Fid0JKQUZJQUt3QTVBSGNBUFFBOUFId0FaUUEyQURRQU9RQmlBRE1BTVFBMkFHRUFZd0JqQURBQU5BQTRBR1VBTlFBeEFHTUFPQUEyQURnQU9BQmtBRGtBWWdCbUFHSUFOZ0JrQUdVQU1nQXdBRFFBWlFBM0FHUUFNZ0EyQURNQU1RQmlBRFFBTlFBNEFHSUFPQUExQURjQU1BQmxBREFBTVFCaEFHSUFZZ0E0QUdJQVlRQmhBREVBT0FBNEFETUFOQUE9DQo="
$Object = [System.Convert]::FromBase64String($PassBase64)
[system.io.file]::WriteAllBytes('C:\temp\TestPassword.txt',$object)

Now we have both files needed to receive our password, let’s convert that password using the key from before. We get the content of the .key file with get-content. Then we use it when we convert to secure string the password file. Pushing all that information into a new credential object for later use in something like a nextcloud download. Then we remove the files as we don’t want that password getting out there.

$Username = "BobTheUser"
$key = Get-Content 'C:\temp\TestPassword.Key'
$MyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, (Get-Content 'c:\temp\TestPassword.txt' | ConvertTo-SecureString -Key $key)
remove-item c:\temp\TestPassword.txt
remove-item c:\temp\TestPassword.key

MSI file

Let’s say we need to embed an MSI file. For example, we have a custom Open DNS MSI that will auto setup the Umbrella client on an end user’s computer. The first thing we want to do is convert that msi file to the base 64 string like above.

$content = Get-Content -Path C:\Temp\CustomOpenDNS.msi -Encoding Bye
[system.Convert]::ToBase64String($Content) | clip.exe

Here we are using the powershell 5 code to convert the msi into a string and pushing it out to our clipboard. Now we need to place that information into the script. This is what the script would look like:

$Base64 = "PCFET0NUWVBFIGh0bWw+CjxodG1sIGNsYXNzPSJuZy1jc3AiIGRhdGEtcGxhY2Vob2xkZXItZm9jdXM9ImZhbHNlIiBsYW5nPSJlbiIgZGF0YS1sb2NhbGU9ImVuIiA+Cgk8aGVhZAogZGF0YS1yZXF1ZXN0dG9rZW49IkZPVkorSVltUkJucnoyNU4zZDZyclhIM0Y5c3NzK3BSOS9HaVN2WFBMZHc9OldLQjd0OHdRSzE2TWx5VVBsNy9ZM0R2T1dMWjk5dHBpcjRlYkxzS3RXTFU9Ij4KCQk8bWV0YSBjaGFyc2V0PSJ1dGYtOCI+CgkJPHRpdGxlPgoJCVJhbmRvbW5lc3MJCTwvdGl0bGU+CgkJPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KCQk8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgoJCQkJPG1ldGEgbmFtZT0iYXBwbGUtaXR1bmVzLWFwcCIgY29udGVudD0iYXBwLWlkPTExMjU0MjAxMDIiPgoJCQkJPG1ldGEgbmFtZT0idGhlbWUtY29sb3IiIGNvbnRlbnQ9IiMwMDIxMzMiPgoJCTxsaW5rIHJlbD0iaWNvbiIgaHJlZj0iL2luZGV4LnBocC9hcHBzL3RoZW1pbmcvZmF2aWNvbj92PTMiPgoJCTxsaW5rIHJlbD0iYXBwbGUtdG91Y2gtaWNvbiIgaHJlZj0iL2luZGV4LnBocC9hcHBzL3RoZW1pbmcvaWNvbj92PTMiPgoJCTxsaW5rIHJlbD0ibWFzay1pY29uIiBzaXplcz0iYW55IiBocmVmPSIvY29yZS9pbWcvZmF2aWNvbi1tYXNrLnN2ZyIgY29sb3I9IiMwMDIxMzMiPgoJCTxsaW5rIHJlbD0ibWFuaWZlc3QiIGhyZWY9Ii9pbmRleC5waHAvYXBwcy90aGVtaW5nL21hbmlmZXN0P3Y9MyI+CgkJPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSIvaW5kZXgucGhwL2Nzcy9jb3JlLzEzM2EtZDJjNi1zZXJ2ZXIuY3NzP3Y9YmUwYjQ2NTY1ZjcwNWM3YmYzNzQ0OTIxMmRmYWQxNzYtYjA3MTM1Y2MtMyI+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2luZGV4LnBocC9jc3MvY29yZS8xMzNhLWQyYzYtY3NzLXZhcmlhYmxlcy5jc3M/dj1iZTBiNDY1NjVmNzA1YzdiZjM3NDQ5MjEyZGZhZDE3Ni1iMDcxMzVjYy0zIj4KPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSIvYXBwcy9maWxlc19yaWdodGNsaWNrL2Nzcy9hcHAuY3NzP3Y9NDdjZDc2ZTQtMyI+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2luZGV4LnBocC9jc3MvdGV4dC8wNzBhLWQyYzYtaWNvbnMuY3NzP3Y9YmUwYjQ2NTY1ZjcwNWM3YmYzNzQ0OTIxMmRmYWQxNzYtYjA3MTM1Y2MtMyI+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2NvcmUvY3NzL2d1ZXN0LmNzcz92PWIwNzEzNWNjLTMiPgo8bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Ii9jb3JlL2Nzcy9wdWJsaWNzaGFyZWF1dGguY3NzP3Y9YjA3MTM1Y2MtMyI+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iL2NvcmUvY3NzL2d1ZXN0LmNzcz92PWIwNzEzNWNjLTMiPgoJCTxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvaW5kZXgucGhwL2NvcmUvanMvb2MuanM/dj1iMDcxMzVjYyI+PC9zY3JpcHQ+CjxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvY29yZS9qcy9kaXN0L21haW4uanM/dj1iMDcxMzVjYy0zIj48L3NjcmlwdD4KPHNjcmlwdCBub25jZT0iUms5V1NpdEpXVzFTUW01eWVqSTFUak5rTm5KeVdFZ3pSamx6YzNNcmNGSTVMMGRwVTNaWVVFeGtkejA2VjB0Q04zUTRkMUZMTVRaTmJIbFZVR3czTDFrelJIWlBWMHhhT1RsMGNHbHlOR1ZpVEhOTGRGZE1WVDA9IiBkZWZlciBzcmM9Ii9jb3JlL2pzL2Rpc3QvZmlsZXNfZmlsZWluZm8uanM/dj1iMDcxMzVjYy0zIj48L3NjcmlwdD4KPHNjcmlwdCBub25jZT0iUms5V1NpdEpXVzFTUW01eWVqSTFUak5rTm5KeVdFZ3pSamx6YzNNcmNGSTVMMGRwVTNaWVVFeGtkejA2VjB0Q04zUTRkMUZMTVRaTmJIbFZVR3czTDFrelJIWlBWMHhhT1RsMGNHbHlOR1ZpVEhOTGRGZE1WVDA9IiBkZWZlciBzcmM9Ii9jb3JlL2pzL2Rpc3QvZmlsZXNfY2xpZW50LmpzP3Y9YjA3MTM1Y2MtMyI+PC9zY3JpcHQ+CjxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvaW5kZXgucGhwL2pzL2NvcmUvbWVyZ2VkLXRlbXBsYXRlLXByZXBlbmQuanM/dj1iMDcxMzVjYy0zIj48L3NjcmlwdD4KPHNjcmlwdCBub25jZT0iUms5V1NpdEpXVzFTUW01eWVqSTFUak5rTm5KeVdFZ3pSamx6YzNNcmNGSTVMMGRwVTNaWVVFeGtkejA2VjB0Q04zUTRkMUZMTVRaTmJIbFZVR3czTDFrelJIWlBWMHhhT1RsMGNHbHlOR1ZpVEhOTGRGZE1WVDA9IiBkZWZlciBzcmM9Ii9jb3JlL2pzL2JhY2tncm91bmRqb2JzLmpzP3Y9YjA3MTM1Y2MtMyI+PC9zY3JpcHQ+CjxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvYXBwcy9maWxlc19zaGFyaW5nL2pzL2Rpc3QvbWFpbi5qcz92PWIwNzEzNWNjLTMiPjwvc2NyaXB0Pgo8c2NyaXB0IG5vbmNlPSJSazlXU2l0SldXMVNRbTV5ZWpJMVRqTmtObkp5V0VnelJqbHpjM01yY0ZJNUwwZHBVM1pZVUV4a2R6MDZWMHRDTjNRNGQxRkxNVFpOYkhsVlVHdzNMMWt6UkhaUFYweGFPVGwwY0dseU5HVmlUSE5MZEZkTVZUMD0iIGRlZmVyIHNyYz0iL2FwcHMvZXB1YnJlYWRlci9qcy9wbHVnaW4uanM/dj1iMDcxMzVjYy0zIj48L3NjcmlwdD4KPHNjcmlwdCBub25jZT0iUms5V1NpdEpXVzFTUW01eWVqSTFUak5rTm5KeVdFZ3pSamx6YzNNcmNGSTVMMGRwVTNaWVVFeGtkejA2VjB0Q04zUTRkMUZMTVRaTmJIbFZVR3czTDFrelJIWlBWMHhhT1RsMGNHbHlOR1ZpVEhOTGRGZE1WVDA9IiBkZWZlciBzcmM9Ii9hcHBzL2ZpbGVzX3ZpZGVvcGxheWVyL2pzL21haW4uanM/dj1iMDcxMzVjYy0zIj48L3NjcmlwdD4KPHNjcmlwdCBub25jZT0iUms5V1NpdEpXVzFTUW01eWVqSTFUak5rTm5KeVdFZ3pSamx6YzNNcmNGSTVMMGRwVTNaWVVFeGtkejA2VjB0Q04zUTRkMUZMTVRaTmJIbFZVR3czTDFrelJIWlBWMHhhT1RsMGNHbHlOR1ZpVEhOTGRGZE1WVDA9IiBkZWZlciBzcmM9Ii9hcHBzL2ZpbGVzX3JpZ2h0Y2xpY2svanMvc2NyaXB0LmpzP3Y9YjA3MTM1Y2MtMyI+PC9zY3JpcHQ+CjxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvYXBwcy9maWxlc19yaWdodGNsaWNrL2pzL2ZpbGVzLmpzP3Y9YjA3MTM1Y2MtMyI+PC9zY3JpcHQ+CjxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvYXBwcy90ZXh0L2pzL3B1YmxpYy5qcz92PWIwNzEzNWNjLTMiPjwvc2NyaXB0Pgo8c2NyaXB0IG5vbmNlPSJSazlXU2l0SldXMVNRbTV5ZWpJMVRqTmtObkp5V0VnelJqbHpjM01yY0ZJNUwwZHBVM1pZVUV4a2R6MDZWMHRDTjNRNGQxRkxNVFpOYkhsVlVHdzNMMWt6UkhaUFYweGFPVGwwY0dseU5HVmlUSE5MZEZkTVZUMD0iIGRlZmVyIHNyYz0iL2FwcHMvdGhlbWluZy9qcy90aGVtaW5nLmpzP3Y9YjA3MTM1Y2MtMyI+PC9zY3JpcHQ+CjxzY3JpcHQgbm9uY2U9IlJrOVdTaXRKV1cxU1FtNXllakkxVGpOa05uSnlXRWd6UmpsemMzTXJjRkk1TDBkcFUzWllVRXhrZHowNlYwdENOM1E0ZDFGTE1UWk5iSGxWVUd3M0wxa3pSSFpQVjB4YU9UbDBjR2x5TkdWaVRITkxkRmRNVlQwPSIgZGVmZXIgc3JjPSIvY29yZS9qcy9wdWJsaWNzaGFyZWF1dGguanM/dj1iMDcxMzVjYy0zIj48L3NjcmlwdD4KCQk8bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Ii9pbmRleC5waHAvY3NzL2ljb25zL2ljb25zLXZhcnMuY3NzP3Y9MTYyOTE3OTQyOSIvPjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgbWVkaWE9IihwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykiIGhyZWY9Ii9pbmRleC5waHAvYXBwcy9hY2Nlc3NpYmlsaXR5L2Nzcy91c2VyLWE4MmZkOTVkYjEwZmYyNWRmYWQzOWYwNzM3MmViZTM3Ii8+PGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSIvaW5kZXgucGhwL2FwcHMvdGhlbWluZy9zdHlsZXM/dj0zIi8+PG1ldGEgbmFtZT0icm9ib3RzIiBjb250ZW50PSJub2luZGV4LCBub2ZvbGxvdyIvPgk8L2hlYWQ+Cgk8Ym9keSBpZD0iYm9keS1sb2dpbiI+CgkJPG5vc2NyaXB0PgoJPGRpdiBpZD0ibm9qYXZhc2NyaXB0Ij4KCQk8ZGl2PgoJCQlUaGlzIGFwcGxpY2F0aW9uIHJlcXVpcmVzIEphdmFTY3JpcHQgZm9yIGNvcnJlY3Qgb3BlcmF0aW9uLiBQbGVhc2UgPGEgaHJlZj0iaHR0cHM6Ly93d3cuZW5hYmxlLWphdmFzY3JpcHQuY29tLyIgdGFyZ2V0PSJfYmxhbmsiIHJlbD0ibm9yZWZlcnJlciBub29wZW5lciI+ZW5hYmxlIEphdmFTY3JpcHQ8L2E+IGFuZCByZWxvYWQgdGhlIHBhZ2UuCQk8L2Rpdj4KCTwvZGl2Pgo8L25vc2NyaXB0PgoJCQkJCTxpbnB1dCB0eXBlPSJoaWRkZW4iIGlkPSJpbml0aWFsLXN0YXRlLXRleHQtd29ya3NwYWNlX2F2YWlsYWJsZSIgdmFsdWU9ImRISjFaUT09Ij4KCQkJCQk8aW5wdXQgdHlwZT0iaGlkZGVuIiBpZD0iaW5pdGlhbC1zdGF0ZS1jb3JlLWNvbmZpZyIgdmFsdWU9ImV5SnpaWE56YVc5dVgyeHBabVYwYVcxbElqb3hORFF3TENKelpYTnphVzl1WDJ0bFpYQmhiR2wyWlNJNmRISjFaU3dpWVhWMGIxOXNiMmR2ZFhRaU9tWmhiSE5sTENKMlpYSnphVzl1SWpvaU1qRXVNQzR4TGpFaUxDSjJaWEp6YVc5dWMzUnlhVzVuSWpvaU1qRXVNQzR4SWl3aVpXNWhZbXhsWDJGMllYUmhjbk1pT25SeWRXVXNJbXh2YzNSZmNHRnpjM2R2Y21SZmJHbHVheUk2Ym5Wc2JDd2liVzlrVW1WM2NtbDBaVmR2Y210cGJtY2lPbVpoYkhObExDSnphR0Z5YVc1bkxtMWhlRUYxZEc5amIyMXdiR1YwWlZKbGMzVnNkSE1pT2pJMUxDSnphR0Z5YVc1bkxtMXBibE5sWVhKamFGTjBjbWx1WjB4bGJtZDBhQ0k2TUN3aVlteGhZMnRzYVhOMFgyWnBiR1Z6WDNKbFoyVjRJam9pWEZ3dUtIQmhjblI4Wm1sc1pYQmhjblFwSkNKOSI+CgkJCQkJPGlucHV0IHR5cGU9ImhpZGRlbiIgaWQ9ImluaXRpYWwtc3RhdGUtY29yZS1jYXBhYmlsaXRpZXMiIHZhbHVlPSJleUpqYjNKbElqcDdJbkJ2Ykd4cGJuUmxjblpoYkNJNk5qQXNJbmRsWW1SaGRpMXliMjkwSWpvaWNtVnRiM1JsTG5Cb2NGd3ZkMlZpWkdGMkluMHNJbUp5ZFhSbFptOXlZMlVpT25zaVpHVnNZWGtpT2pCOUxDSm1hV3hsY3lJNmV5SmlhV2RtYVd4bFkyaDFibXRwYm1jaU9uUnlkV1VzSW1Kc1lXTnJiR2x6ZEdWa1gyWnBiR1Z6SWpwYklpNW9kR0ZqWTJWemN5SmRMQ0prYVhKbFkzUkZaR2wwYVc1bklqcDdJblZ5YkNJNkltaDBkSEJ6T2x3dlhDOXlibVJoWkdoa2JXRnVMbU52YlZ3dmIyTnpYQzkyTWk1d2FIQmNMMkZ3Y0hOY0wyWnBiR1Z6WEM5aGNHbGNMM1l4WEM5a2FYSmxZM1JGWkdsMGFXNW5JaXdpWlhSaFp5STZJall5TWpaaVlUZzNNek0zTTJZMVpUY3pZVE5sWmpVd05ERXdOelV5TTJZM0luMHNJbU52YlcxbGJuUnpJanAwY25WbExDSjFibVJsYkdWMFpTSTZkSEoxWlN3aWRtVnljMmx2Ym1sdVp5STZkSEoxWlgwc0ltRmpkR2wyYVhSNUlqcDdJbUZ3YVhZeUlqcGJJbVpwYkhSbGNuTWlMQ0ptYVd4MFpYSnpMV0Z3YVNJc0luQnlaWFpwWlhkeklpd2ljbWxqYUMxemRISnBibWR6SWwxOUxDSnZZMjBpT25zaVpXNWhZbXhsWkNJNmRISjFaU3dpWVhCcFZtVnljMmx2YmlJNklqRXVNQzF3Y205d2IzTmhiREVpTENKbGJtUlFiMmx1ZENJNkltaDBkSEJ6T2x3dlhDOXlibVJoWkdoa2JXRnVMbU52YlZ3dmFXNWtaWGd1Y0dod1hDOXZZMjBpTENKeVpYTnZkWEpqWlZSNWNHVnpJanBiZXlKdVlXMWxJam9pWm1sc1pTSXNJbk5vWVhKbFZIbHdaWE1pT2xzaWRYTmxjaUlzSW1keWIzVndJbDBzSW5CeWIzUnZZMjlzY3lJNmV5SjNaV0prWVhZaU9pSmNMM0IxWW14cFl5NXdhSEJjTDNkbFltUmhkbHd2SW4xOVhYMHNJbVJoZGlJNmV5SmphSFZ1YTJsdVp5STZJakV1TUNKOUxDSm1kV3hzZEdWNGRITmxZWEpqYUNJNmV5SnlaVzF2ZEdVaU9uUnlkV1VzSW5CeWIzWnBaR1Z5Y3lJNlcxMTlMQ0p1YjNScFptbGpZWFJwYjI1eklqcDdJbTlqY3kxbGJtUndiMmx1ZEhNaU9sc2liR2x6ZENJc0ltZGxkQ0lzSW1SbGJHVjBaU0lzSW1SbGJHVjBaUzFoYkd3aUxDSnBZMjl1Y3lJc0luSnBZMmd0YzNSeWFXNW5jeUlzSW1GamRHbHZiaTEzWldJaUxDSjFjMlZ5TFhOMFlYUjFjeUpkTENKd2RYTm9JanBiSW1SbGRtbGpaWE1pTENKdlltcGxZM1F0WkdGMFlTSXNJbVJsYkdWMFpTSmRMQ0poWkcxcGJpMXViM1JwWm1sallYUnBiMjV6SWpwYkltOWpjeUlzSW1Oc2FTSmRmU3dpY0dGemMzZHZjbVJmY0c5c2FXTjVJanA3SW0xcGJreGxibWQwYUNJNk9Dd2laVzVtYjNKalpVNXZia052YlcxdmJsQmhjM04zYjNKa0lqcDBjblZsTENKbGJtWnZjbU5sVG5WdFpYSnBZME5vWVhKaFkzUmxjbk1pT21aaGJITmxMQ0psYm1admNtTmxVM0JsWTJsaGJFTm9ZWEpoWTNSbGNuTWlPbVpoYkhObExDSmxibVp2Y21ObFZYQndaWEpNYjNkbGNrTmhjMlVpT21aaGJITmxMQ0poY0draU9uc2laMlZ1WlhKaGRHVWlPaUpvZEhSd2N6cGNMMXd2Y201a1lXUm9aRzFoYmk1amIyMWNMMjlqYzF3dmRqSXVjR2h3WEM5aGNIQnpYQzl3WVhOemQyOXlaRjl3YjJ4cFkzbGNMMkZ3YVZ3dmRqRmNMMmRsYm1WeVlYUmxJaXdpZG1Gc2FXUmhkR1VpT2lKb2RIUndjenBjTDF3dmNtNWtZV1JvWkcxaGJpNWpiMjFjTDI5amMxd3Zkakl1Y0dod1hDOWhjSEJ6WEM5d1lYTnpkMjl5WkY5d2IyeHBZM2xjTDJGd2FWd3ZkakZjTDNaaGJHbGtZWFJsSW4xOUxDSndjbTkyYVhOcGIyNXBibWRmWVhCcElqcDdJblpsY25OcGIyNGlPaUl4TGpFeExqQWlMQ0pCWTJOdmRXNTBVSEp2Y0dWeWRIbFRZMjl3WlhOV1pYSnphVzl1SWpveUxDSkJZMk52ZFc1MFVISnZjR1Z5ZEhsVFkyOXdaWE5HWldSbGNtRjBhVzl1Ulc1aFlteGxaQ0k2ZEhKMVpYMHNJbVpwYkdWelgzTm9ZWEpwYm1jaU9uc2ljMmhoY21WaWVXMWhhV3dpT25zaVpXNWhZbXhsWkNJNmRISjFaU3dpZFhCc2IyRmtYMlpwYkdWelgyUnliM0FpT25zaVpXNWhZbXhsWkNJNmRISjFaWDBzSW5CaGMzTjNiM0prSWpwN0ltVnVZV0pzWldRaU9uUnlkV1VzSW1WdVptOXlZMlZrSWpwbVlXeHpaWDBzSW1WNGNHbHlaVjlrWVhSbElqcDdJbVZ1WVdKc1pXUWlPblJ5ZFdWOWZTd2lZWEJwWDJWdVlXSnNaV1FpT25SeWRXVXNJbkIxWW14cFl5STZleUpsYm1GaWJHVmtJanAwY25WbExDSndZWE56ZDI5eVpDSTZleUpsYm1admNtTmxaQ0k2Wm1Gc2MyVXNJbUZ6YTBadmNrOXdkR2x2Ym1Gc1VHRnpjM2R2Y21RaU9tWmhiSE5sZlN3aVpYaHdhWEpsWDJSaGRHVWlPbnNpWlc1aFlteGxaQ0k2Wm1Gc2MyVjlMQ0p0ZFd4MGFYQnNaVjlzYVc1cmN5STZkSEoxWlN3aVpYaHdhWEpsWDJSaGRHVmZhVzUwWlhKdVlXd2lPbnNpWlc1aFlteGxaQ0k2Wm1Gc2MyVjlMQ0p6Wlc1a1gyMWhhV3dpT21aaGJITmxMQ0oxY0d4dllXUWlPblJ5ZFdVc0luVndiRzloWkY5bWFXeGxjMTlrY205d0lqcDBjblZsZlN3aWNtVnphR0Z5YVc1bklqcDBjblZsTENKMWMyVnlJanA3SW5ObGJtUmZiV0ZwYkNJNlptRnNjMlVzSW1WNGNHbHlaVjlrWVhSbElqcDdJbVZ1WVdKc1pXUWlPblJ5ZFdWOWZTd2laM0p2ZFhCZmMyaGhjbWx1WnlJNmRISjFaU3dpWjNKdmRYQWlPbnNpWlc1aFlteGxaQ0k2ZEhKMVpTd2laWGh3YVhKbFgyUmhkR1VpT25zaVpXNWhZbXhsWkNJNmRISjFaWDE5TENKa1pXWmhkV3gwWDNCbGNtMXBjM05wYjI1eklqb3pNU3dpWm1Wa1pYSmhkR2x2YmlJNmV5SnZkWFJuYjJsdVp5STZkSEoxWlN3aWFXNWpiMjFwYm1jaU9uUnlkV1VzSW1WNGNHbHlaVjlrWVhSbElqcDdJbVZ1WVdKc1pXUWlPblJ5ZFdWOWZTd2ljMmhoY21WbElqcDdJbkYxWlhKNVgyeHZiMnQxY0Y5a1pXWmhkV3gwSWpwbVlXeHpaU3dpWVd4M1lYbHpYM05vYjNkZmRXNXBjWFZsSWpwMGNuVmxmWDBzSW5Sb1pXMXBibWNpT25zaWJtRnRaU0k2SWxKaGJtUnZiVzVsYzNNaUxDSjFjbXdpT2lKb2RIUndjenBjTDF3dmJtVjRkR05zYjNWa0xtTnZiU0lzSW5Oc2IyZGhiaUk2SWtKbGFHOXNaQ0JYYjNKc1pDQnZaaUIzWlhRZ2MyOWphM01pTENKamIyeHZjaUk2SWlNd01ESXhNek1pTENKamIyeHZjaTEwWlhoMElqb2lJMlptWm1abVppSXNJbU52Ykc5eUxXVnNaVzFsYm5RaU9pSWpNREF5TVRNeklpd2lZMjlzYjNJdFpXeGxiV1Z1ZEMxaWNtbG5hSFFpT2lJak1EQXlNVE16SWl3aVkyOXNiM0l0Wld4bGJXVnVkQzFrWVhKcklqb2lJelUxTlRVMU5TSXNJbXh2WjI4aU9pSm9kSFJ3Y3pwY0wxd3ZjbTVrWVdSb1pHMWhiaTVqYjIxY0wyTnZjbVZjTDJsdFoxd3ZiRzluYjF3dmJHOW5ieTV6ZG1jL2RqMHpJaXdpWW1GamEyZHliM1Z1WkNJNklpTXdNREl4TXpNaUxDSmlZV05yWjNKdmRXNWtMWEJzWVdsdUlqcDBjblZsTENKaVlXTnJaM0p2ZFc1a0xXUmxabUYxYkhRaU9uUnlkV1VzSW14dloyOW9aV0ZrWlhJaU9pSm9kSFJ3Y3pwY0wxd3ZjbTVrWVdSb1pHMWhiaTVqYjIxY0wyTnZjbVZjTDJsdFoxd3ZiRzluYjF3dmJHOW5ieTV6ZG1jL2RqMHpJaXdpWm1GMmFXTnZiaUk2SW1oMGRIQnpPbHd2WEM5eWJtUmhaR2hrYldGdUxtTnZiVnd2WTI5eVpWd3ZhVzFuWEM5c2IyZHZYQzlzYjJkdkxuTjJaejkyUFRNaWZTd2lkWE5sY2w5emRHRjBkWE1pT25zaVpXNWhZbXhsWkNJNmRISjFaU3dpYzNWd2NHOXlkSE5mWlcxdmFta2lPblJ5ZFdWOUxDSjNaV0YwYUdWeVgzTjBZWFIxY3lJNmV5SmxibUZpYkdWa0lqcDBjblZsZlgwPSI+CgkJCQkJPGlucHV0IHR5cGU9ImhpZGRlbiIgaWQ9ImluaXRpYWwtc3RhdGUtdGhlbWluZy1kYXRhIiB2YWx1ZT0iZXlKdVlXMWxJam9pVW1GdVpHOXRibVZ6Y3lJc0luVnliQ0k2SW1oMGRIQnpPbHd2WEM5dVpYaDBZMnh2ZFdRdVkyOXRJaXdpYzJ4dloyRnVJam9pUW1Wb2IyeGtJRmR2Y214a0lHOW1JSGRsZENCemIyTnJjeUlzSW1OdmJHOXlJam9pSXpBd01qRXpNeUlzSW1sdGNISnBiblJWY213aU9pSWlMQ0p3Y21sMllXTjVWWEpzSWpvaUlpd2lhVzUyWlhKMFpXUWlPbVpoYkhObExDSmpZV05vWlVKMWMzUmxjaUk2SWpNaWZRPT0iPgoJCQkJCTxpbnB1dCB0eXBlPSJoaWRkZW4iIGlkPSJpbml0aWFsLXN0YXRlLWFjY2Vzc2liaWxpdHktZGF0YSIgdmFsdWU9ImV5SjBhR1Z0WlNJNlptRnNjMlVzSW1ocFoyaGpiMjUwY21GemRDSTZabUZzYzJWOSI+CgkJCQk8ZGl2IGNsYXNzPSJ3cmFwcGVyIj4KCQkJPGRpdiBjbGFzcz0idi1hbGlnbiI+CgkJCQkJCQkJCTxoZWFkZXIgcm9sZT0iYmFubmVyIj4KCQkJCQkJPGRpdiBpZD0iaGVhZGVyIj4KCQkJCQkJCTxkaXYgY2xhc3M9ImxvZ28iPgoJCQkJCQkJCTxoMSBjbGFzcz0iaGlkZGVuLXZpc3VhbGx5Ij4KCQkJCQkJCQkJUmFuZG9tbmVzcwkJCQkJCQkJPC9oMT4KCQkJCQkJCQkJCQkJCQkJPC9kaXY+CgkJCQkJCTwvZGl2PgoJCQkJCTwvaGVhZGVyPgoJCQkJCQkJCTxtYWluPgoJCQkJCTxmb3JtIG1ldGhvZD0icG9zdCI+Cgk8ZmllbGRzZXQgY2xhc3M9Indhcm5pbmciPgoJCQkJCTxkaXYgY2xhc3M9Indhcm5pbmctaW5mbyI+VGhpcyBzaGFyZSBpcyBwYXNzd29yZC1wcm90ZWN0ZWQ8L2Rpdj4KCQkJCQkJPHA+CgkJCTxsYWJlbCBmb3I9InBhc3N3b3JkIiBjbGFzcz0iaW5maWVsZCI+UGFzc3dvcmQ8L2xhYmVsPgoJCQk8aW5wdXQgdHlwZT0iaGlkZGVuIiBuYW1lPSJyZXF1ZXN0dG9rZW4iIHZhbHVlPSJGT1ZKK0lZbVJCbnJ6MjVOM2Q2cnJYSDNGOXNzcytwUjkvR2lTdlhQTGR3PTpXS0I3dDh3UUsxNk1seVVQbDcvWTNEdk9XTFo5OXRwaXI0ZWJMc0t0V0xVPSIgLz4KCQkJPGlucHV0IHR5cGU9InBhc3N3b3JkIiBuYW1lPSJwYXNzd29yZCIgaWQ9InBhc3N3b3JkIgoJCQkJcGxhY2Vob2xkZXI9IlBhc3N3b3JkIiB2YWx1ZT0iIgoJCQkJYXV0b2NvbXBsZXRlPSJuZXctcGFzc3dvcmQiIGF1dG9jYXBpdGFsaXplPSJvZmYiIGF1dG9jb3JyZWN0PSJvZmYiCgkJCQlhdXRvZm9jdXMgLz4KCQkJPGlucHV0IHR5cGU9ImhpZGRlbiIgbmFtZT0ic2hhcmluZ1Rva2VuIiB2YWx1ZT0iNmZMbnFvdDZEcW02b1RYIiBpZD0ic2hhcmluZ1Rva2VuIj4KCQkJPGlucHV0IHR5cGU9InN1Ym1pdCIgaWQ9InBhc3N3b3JkLXN1Ym1pdCIgCgkJCQljbGFzcz0ic3ZnIGljb24tY29uZmlybSBpbnB1dC1idXR0b24taW5saW5lIiB2YWx1ZT0iIiBkaXNhYmxlZD0iZGlzYWJsZWQiIC8+CgkJPC9wPgoJPC9maWVsZHNldD4KPC9mb3JtPgoJCQkJPC9tYWluPgoJCQk8L2Rpdj4KCQk8L2Rpdj4KCQk8Zm9vdGVyIHJvbGU9ImNvbnRlbnRpbmZvIj4KCQkJPHAgY2xhc3M9ImluZm8iPgoJCQkJPGEgaHJlZj0iaHR0cHM6Ly9uZXh0Y2xvdWQuY29tIiB0YXJnZXQ9Il9ibGFuayIgcmVsPSJub3JlZmVycmVyIG5vb3BlbmVyIiBjbGFzcz0iZW50aXR5LW5hbWUiPlJhbmRvbW5lc3M8L2E+IOKAkyBCZWhvbGQgV29ybGQgb2Ygd2V0IHNvY2tzCQkJPC9wPgoJCTwvZm9vdGVyPgoJPC9ib2R5Pgo8L2h0bWw+Cg==
"
$Object = [System.Convert]::FromBase64String($Base64)
[system.io.file]::WriteAllBytes("C:\temp\test.msi",$object)
msiexec /i C:\temp\test.msi /qn /norestart

As you can see by the size of the base64 string, that seems like a large file. Surprisingly that’s only 11kb in size. The larger the file the more code will be present and the chances of buffer overload increases.

Scripts

What kind of person would I be without giving you a function or two to make life easier on you. Here are two functions. The first converts the item to base64 string and copies it to your clipboard. The second converts a base64 string to an object.

function convertto-base64 {
    [cmdletbinding()]
    param (
        [string]$Path
    )
    process {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $Content = Get-Content -Path $Path -AsByteStream -raw
        } else {
            $Content = Get-Content -Path $Path -Encoding Byte
        }
        $base64 = [system.convert]::ToBase64String($content)
        $Base64 | clip.exe
    }
}

Here is the convert back to the object script. You will need the string and an out path.

function Convertfrom-base64 {
    [cmdletbinding()]
    param (
        [string]$Base64String,
        [string]$OutFile
    )
    process {
        $Object = [System.Convert]::FromBase64String($Base64String)
        [system.io.file]::WriteAllBytes($OutFile,$object)
    }
}
Install Google Chrome with Powershell

Install Google Chrome with Powershell

My last weekly challenge to myself was to deploy google chrome with Powershell. This one was the hardest because it was hard to find the latest and greatest MSI 64 bit of google chrome. The coding wasn’t too hard after that. How google chrome works, follows suit the same way as firefox did.

The Script

$ChromeSource = "http://dl.google.com/tag/s/defaultbrowser/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi"
$Installer = "$ENV:TEMP\Google.msi"
Invoke-WebRequest -Uri $ChromeSource -OutFile $Installer
Get-Process -Name "Chrome" | Stop-Process -Force
msiexec /i $Installer /qn /norestart
Remove-Item $Installer

The Breakdown

We first start off getting the latest enterprise edition of google chrome. This is the 64-bit version thus the most stable version on them all. This is the version I would install everywhere in a company if I had a choice.

$ChromeSource = "http://dl.google.com/tag/s/defaultbrowser/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi"

Then we create the installer path and download the file using the Invoke-webrequest with the Outfile as the installer path.

$Installer = "$ENV:TEMP\Google.msi"
Invoke-WebRequest -Uri $ChromeSource -OutFile $Installer

Then we stop the google chrome process and install the latest version of google chrome using the msiexec command. Then we remove the installer.

Get-Process -Name "Chrome" | Stop-Process -Force
msiexec /i $Installer /qn /norestart
Remove-Item $Installer

That’s it. Very simple very straightforward. If you need to uninstall before installing (My testing was a success with overlapping the installers) then you can do the following before Installing google chrome:

$Chrome = Get-CimInstance win32_Product | where-object {$_.name -like "*Google*Chrome*"}
$Chrome | Invoke-CimMethod -MethodName Uninstall

Install Firefox with Powershell

Install Firefox with Powershell

Need to deploy the latest version of firefox to 1000 machines, Here is a little powerhouse script that can do just that. It’s similar to my last script (Gimp) as it downloads directly from the web. This time we don’t have to parse out a website since Modzilla has it laid out before us.

The Script

$FirefoxSource = "https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US"
$Installer = "$ENV:TEMP\ModzillaFirefox.exe"
Invoke-WebRequest -Uri $FirefoxSource -OutFile $Installer
Get-Process -Name "*firefox*" | Stop-Process -Force
Start-Process -FilePath $Installer -ArgumentList "/s" -Verb runas -wait
Remove-Item $Installer

The Breakdown

The source is awesome. We are downloading directly from the site with their latest 64-bit product. This time we are going with the temporary file and then downloading the file with invoke-webrequest. Then we start the process of installing it with the /s flag which means silent. Since we are coming from the temp folder I threw in the runas flag to run it as the system. This way it installs for all users. Next, we set the wait flag to install the system. From everything I have read, we don’t need to uninstall the previous version to install the newest version. We do however have to stop the process that’s why we have a get process and stop process above. Finally, we remove the installer. That’s it. A lot simpler than Gimp.

Install Gimp with Powershell

Install Gimp with Powershell

This little script installs the latest version of gimp 2.10 onto your Windows machine. Let’s take a look at the script and then break it down.

The Script

$DownloadPath = "C:\Temp\Gimp"
If (!(Test-Path -Path "C:\Temp\")) {New-Item -Path c:\ -Name Temp -ItemType Directory }
If (!(Test-Path -Path "C:\Temp\Gimp\")) {New-Item -Path c:\Temp -Name Gimp -ItemType Directory }
$URL = "https://download.gimp.org/mirror/pub/gimp/v2.10/windows/"
$Gimp = Invoke-WebRequest -UseBasicParsing -Uri $URL -SessionVariable websession
$Links = $Gimp.Links | Where-Object {$_.href -like "*.exe"} | select-object -Last 1
$URLDownload = "$URL$($Links.href)"
$DownloadName = "$DownloadPath\Gimp.exe"
Invoke-WebRequest -Uri $URLDownload -OutFile $DownloadName
if (Test-path "C:\Program Files\GIMP 2") {
    Get-Process -Name "Gimp*" | Stop-Process
    Start-Process -FilePath "C:\Program Files\GIMP 2\uninst\unins000.exe" -ArgumentList "/VERYSILENT" -wait
}
Start-Process -FilePath $DownloadName -ArgumentList '/VERYSILENT /NORESTART /ALLUSERS' -wait
Remove-Item $DownloadName

The Breakdown

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.

$DownloadPath = "C:\Temp\Gimp"
If (!(Test-Path -Path "C:\Temp\")) {New-Item -Path c:\ -Name Temp -ItemType Directory }
If (!(Test-Path -Path "C:\Temp\Gimp\")) {New-Item -Path c:\Temp -Name Gimp -ItemType Directory }

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.

$URL = "https://download.gimp.org/mirror/pub/gimp/v2.10/windows/"

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.

$Gimp = Invoke-WebRequest -UseBasicParsing -Uri $URL -SessionVariable websession
$Links = $Gimp.Links | Where-Object {$_.href -like "*.exe"} | select-object -Last 1

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 $().

$URLDownload = "$URL$($Links.href)"
$DownloadName = "$DownloadPath\Gimp.exe"

Next we download the Gimp 2.10 version we are wanting with another invoke-webrequest. This time we select the Outfile tab.

Invoke-WebRequest -Uri $URLDownload -OutFile $DownloadName

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.

if (Test-path "C:\Program Files\GIMP 2") {
    Get-Process -Name "Gimp*" | Stop-Process -Force
    Start-Process -FilePath "C:\Program Files\GIMP 2\uninst\unins000.exe" -ArgumentList "/VERYSILENT" -Wait
}

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.

Start-Process -FilePath $DownloadName -ArgumentList '/VERYSILENT /NORESTART /ALLUSERS' -Wait

Finally we remove the installer with a remote-item.

Remove-Item $DownloadName

That’s all it takes yall. I hope this is helpful to you.

Webroot Deployment

Webroot Deployment

In this article, I will guide you through the process of deploying webroot via Group Policy. This is a fairly straightforward process with only a little editing of the MSI. I am assuming you know how to download the MSI from the webroot portal. The portal changes often, so, I will leave this part out. If you are ready, throw on your group policy pins, and let’s get started.

Super Orca

The first thing you will need is the Super Orca. You can download it here, link. Once you get super orca installed, we will be able to download and set up the webroot MSI.

  1. Open Super Orca
  2. Open the Webroot MSI.
  3. Click the Property On the left (Red Block).
  4. Click GUILIC (Green Block)
  5. Enter the Key Number
  6. Click File
  7. Click Save As
  8. Save as a different name. ALWAYS KEEP THE ORIGINAL!

Shared Folder

Now you have the MSI ready. You need to place it into a shared folder location. This location has to be accessible to every computer in the company as a minimum of read-only. Make sure the share is shared! I can’t tell you how many times I made this mistake. If it’s shared, good, make sure some of the clients can reach it.

Group Policy

Now we have the MSI ready to go. It’s time for the group policy. It’s a very simple computer policy. In my experience, a lot of IT managers don’t want AVs on servers. So, this tutorial will include a wmi filter. Let’s get to it.

  1. Open Group Policy.
  2. Create a new policy and name it Workstation Webroot Deployment
  3. Right click the policy and select edit
  4. Computer > Policies > Software Settings > Software Installation
  5. Right Click and select New.
  6. Select the file for the installer.
  7. Click Ok
  8. Exit the policy

Now the WMI Filter.

  1. Double click the policy
  2. Right Click the WMI Filter (Purple Arrow)
  3. Click Add (Red Arrow)
  4. Enter: select * from win32_operatingsystem where producttype = 1
    1. The numbers mean:
      1. Workstation
      2. Domain Controller
      3. Server
  5. Click Ok
  6. Under the WMI Filter Select the WMI Object.

All that is left is linking the GPO. Now you can link it wherever you want. Most orgs have an OU just for workstations and one for servers just for this case. It doesn’t matter where you link it the WMI filter will ignore servers and only hit the workstations.

As always, if you have questions, feel free to ask. If you ever see anything that is wrong, feel free to reach out and correct me. Thank you for reading.

Citrix Workspace Installer Script

Citrix Workspace Installer Script

I don’t like working with Citrix receiver. They drive me crazy. One version doesn’t work with the other and so on and so forth. Then finding the one you need is a pain. Thankfully, the workspace is a little better at this process. Many of my clients have recently updated their back end so the new workspace will work for them. It only took a while. So, I built a script that automatically downloads the newest version and installs it accordingly. It wasn’t until later did I realize someone else did this already. But the one I made is a little better as it doesn’t run into the conflict of pulling the version number, at least in my humble opinion. This time we will start off with the script for us lazy admins. If you want to learn how it works, keep reading on.

The Script

IF (!(Test-Path c:\temp)){New-Item -Path c:\ -Name Temp -ItemType "directory"} 
IF (!(Test-Path c:\temp\Citrix)) {New-Item -Path c:\temp -Name Citrix -ItemType "directory"} 
$StartTime = (Get-Date).tostring("yyyy-MM-dd_hh-mm-ss")
$Logname = "C:\temp\Citrix\Install_$StartTime.log"
$DownloadFullPath = "C:\temp\Citrix\Installer_$StartTime.exe"
"Log: $($startTime): Started" > $Logname
try {
    $CitrixPage = Invoke-WebRequest -UseBasicParsing -Uri ("https://www.citrix.com/downloads/workspace-app/windows/workspace-app-for-windows-latest.html") -SessionVariable websession
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Accessed" >> $Logname
} catch {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Failed to access" >> $Logname
    Write-Error "Site Error: Site not accessible"
    Break
}
$DownloadLink = $CitrixPage.Links | Where-Object {$_.rel -like "*CitrixWorkspaceApp.exe*"}
$URL = "Https:$($DownloadLink.rel)"
try {
    Invoke-WebRequest -Uri $URL -OutFile $DownloadFullPath
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Download $URL to $DownloadFullPath" >> $Logname
} catch {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Failed to download $URL to $DownloadFullPath" >> $Logname
    Write-Error "Site Error: Download Failure"
    Break
}
try {
    $Install = Start-Process -FilePath $DownloadFullPath -ArgumentList '/silent /forceinstall /AutoUpdateCheck=disabled /noreboot' -PassThru -ErrorAction Stop
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Install: $($LogTime): Installing $DownloadFullPath" >> $Logname
} catch {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Install: $($LogTime): $DownloadFullPath Failed to Install" >> $Logname
    Write-Error "Install Error"
    Break
}
$LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
"Sleep: Sleep for 420 Seconds for install" >> $Logname
Start-Sleep -Seconds 420
$LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
"Sleep: Stop Sleep" >> $Logname
$Programs = Get-CimInstance -ClassName win32_product
$Citrix = $Programs | where-object {$_.name -like "Citrix*Workspace*Browser"}
if ($null -ne $Citrix) {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Check: $($LogTime): $($Citrix.Caption) - $($Citrix.Version) Installed on: $($Citrix.Installdate.tostring())" >> $Logname
} else {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Check: $($LogTime): Install Failed" >> $Logname
}
Remove-Item -Path $DownloadFullPath -Force

The Break Down

Lets break this guy down. The first part is we are testing if the c:\temp folder exists. If it doesn’t then we will create it. Then we test if the Citrix folder exists, if it doesn’t, once again, we create it. We do this with the Test-Path for testing and the New-Item cmdlets.

IF (!(Test-Path c:\temp)){New-Item -Path c:\ -Name Temp -ItemType "directory"} 
IF (!(Test-Path c:\temp\Citrix)) {New-Item -Path c:\temp -Name Citrix -ItemType "directory"} 

Now we have the folders we will be using created, we need to move to creating the first log entry. We want this log to have a timestamp on it that matches the downloaded installer. To do this, we need to get the datetime first. While doing that we will create the filename of the log and the file name of the download path. This way it’s easier to work with later on in the script. We do this by using the Get-Date cmdlet. Normally the Get-Date cmdlet outputs is an object. Which isn’t very useful in a file name since it contains forbidden characters. (Not forbidden like slifer the sky dragon). A translation is required. We do this with the .tostring() method. Notice the way we format it.

  • y = year
  • M = Month
  • d = day
  • h = hour
  • m = minute
  • s = seconds
$StartTime = (Get-Date).tostring("yyyy-MM-dd_hh-mm-ss")

We then use the $StartTime variable inside the log name and the download pathname. This is done by a string with the variable inside of it. Next will be to create the log. We do this with a simple > which means out and create. >> means out and append. Notice in the example below we $($StartTime) we do this because the next character is a :. Inside PowerShell, you can do things like $Global:Var which tells the shell to keep that var in memory for other functions to use. This means the : is a command character. This is why we wrap the start time variable inside a $(). Powershell will only print what is inside the $(). Finally, take note of the > $Logname. We will be using $Logname more inside this script. This is why we created the variable.

$Logname = "C:\temp\Citrix\Install_$StartTime.log"
$DownloadFullPath = "C:\temp\Citrix\Installer_$StartTime.exe"
"Log: $($startTime): Started" > $Logname

Now we have the start of the log. It’s time to get the installer. In the past, we would just go to the download link and add that to a download script. However, recently Citrix changed how they download. They have tacked on an additional piece of code. Everything past the GDA is that special code they have tacked on to stop direct downloading. However, we have PowerShell on our side.

https://downloads.citrix.com/19176/CitrixWorkspaceApp.exe?__gda__=1615916903_06373f7510a0edd3a06ef41c13dbe8a7

The first thing we want to do is setup a try catch. This way we can catch errors and log them. Also we can break the script with an error message that is useful. This way if you are deploying out with something like continuum or PDQ your error message makes sense. Inside the try, we want to get the webpage itself. Then log that we grabbed information. The cmdlet to get the website is Invoke-webrequest. In the below example I am using the -usebasicparsing because it’s more compatible with websites and with systems. My goal is to launch this thing to 100+ machines. The -Uri is for the website itself and finally, we use the -sessionvariable as a websession. This allows us to grab data easier, especially if it’s auto-generated, like in this case.

$CitrixPage = Invoke-WebRequest -UseBasicParsing -Uri ("https://www.citrix.com/downloads/workspace-app/windows/workspace-app-for-windows-latest.html") -SessionVariable websession

After we grab the website, we have to log the event. We do the same thing we did with $StartTime and place it in the file we created a few moments ago.

$LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
"Site: $($LogTime): Accessed" >> $Logname

If these commands fail for whatever reason, the website is down, the internet is blocking, anything, we need to know that the site can’t be reached. This is why we have a log. We create the same as the $logime but this time we also add a write-error and a break command. The write-error command will send an error to a deployment software, This way we know what’s going on. The break command breaks the script at that point and doesn’t continue.

$LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
"Site: $($LogTime): Failed to access" >> $Logname
Write-Error "Site Error: Site not accessible"
Break

Lets put them together inside the try catch so you can see what it looks like.

try {
    $CitrixPage = Invoke-WebRequest -UseBasicParsing -Uri ("https://www.citrix.com/downloads/workspace-app/windows/workspace-app-for-windows-latest.html") -SessionVariable websession -DisableKeepAlive
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Accessed" >> $Logname
} catch {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Failed to access" >> $Logname
    Write-Error "Site Error: Site not accessible"
    Break
}

Now we have the website itself inside a variable. It’s time to find what we need. The $CitrixPage contains different elements inside of it. Each item can give you information. The Rawcontent is just like it sounds, raw content of the page. While status code can give you information about if the site is up or what condition it is in. In this case, we will be looking at the links and status code. We check if the site has a good status of 200, if it doesn’t, then we don’t want to battle that battle. Thus we log and break like before. If it does, however, we want to take apart the links and find the one that contains the exe that we need. We do this with a where-object cmdlet. We search the .rel for the *CitrixWorkspaceApp.exe. Because the .links sometimes produces incomplete links, we have to build them. That’s the second step is to build the link. We will wrap the outcome inside an https: string.

if ($CitrixPage.statuscode -eq 200) {
    $DownloadLink = $CitrixPage.Links | Where-Object {$_.rel -like "*CitrixWorkspaceApp.exe*"}
    $URL = "Https:$($DownloadLink.rel)"
} else {
    "Site: $($LogTime): Site Status Code $($CitrixPage.StatusCode)" >> $Logname
    Write-Error "Site Error: Status Code $($CitrixPage.StatusCode)"
    Break
}

Now we have the custom URL for the download, we need to download the file itself. Remember the $DownloadFullPath we created a while ago. It’s time to use it. We will be using the invoke-webrequest once again as well. This time we will use the -OutFile cmdlet. This cmdlet of invoke-webrequest will download the file as requested from the url provided. Of course, we want to wrap all of this inside of a try catch. This way we can log correctly and break as needed.

try {
    Invoke-WebRequest -Uri $URL -OutFile $DownloadFullPath
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Download $URL to $DownloadFullPath" >> $Logname
} catch {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Site: $($LogTime): Failed to download $URL to $DownloadFullPath" >> $Logname
    Write-Error "Site Error: Download Failure"
    Break
}

Now we have the installer to work with. The filename has the same time/date stamp as the log file so we can compare if the script doesn’t finish up correctly. Next we will start the process of another try catch to install the program. The command we will use is the Start-Process command. We start the $DownloadFullPath for the file name. We want this thing to be quiet and overwrite everything else there. Citrix, if given the /forceinstall will force the install by uninstalling the last version. Finally we tell it not to reboot with the /noreboot. Once we get past the arguments, we want to make sure we have the information from this thus we put the -passthru flag. This will allow us to store the information into a variable if we want to use that information later. The final part of the command is the -erroraction. We want this thing to stop if it hits an error. This way we know that something is broken. Then we log accordingly and catch accordingly like above.

try {
    $Install = Start-Process -FilePath $DownloadFullPath -ArgumentList '/silent /forceinstall /AutoUpdateCheck=disabled /noreboot' -PassThru -ErrorAction Stop
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Install: $($LogTime): Installing $DownloadFullPath" >> $Logname
} catch {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Install: $($LogTime): $DownloadFullPath Failed to Install" >> $Logname
    Write-Error "Install Error"
    Break
}

We are almost done! This program takes an average of 5 minutes on older machines to install. Thus we sleep for 7 minutes. To do this we use the command Start-Sleep and set the -seconds to 420 seconds. We also make sure we log this information.

$LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
"Sleep: Sleep for 420 Seconds for install" >> $Logname
Start-Sleep -Seconds 420
$LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
"Sleep: Stop Sleep" >> $Logname

As we are not in a hurry, we use the PowerShell command Get-CimInstance to get the products and sort through that to find the Citrix Workplace Browser using the where-object cmdlet.

$Programs = Get-CimInstance -ClassName win32_product
$Citrix = $Programs | where-object {$_.name -like "Citrix*Workspace*Browser"}

Finally we check to see if the install was successful or not. This is done with a simple $null -ne $something. We do it this way because we first load nothing and start to compare nothing to something. if something is there, then we know the statement is true and stop processing. Very simple concept. If $Citrix does contain something we log that the install was successful and remove the installer. If we find $Citrix is $null, then we log the error and error out once again.

if ($null -ne $Citrix) {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Check: $($LogTime): $($Citrix.Caption) - $($Citrix.Version) Installed on: $($Citrix.Installdate.tostring())" >> $Logname
    Remove-Item -Path $DownloadFullPath -Force
} else {
    $LogTime = (Get-date).tostring("yyyy:MM:dd-hh:mm:ss")
    "Check: $($LogTime): Install Failed" >> $Logname
    Write-Error "Install: Install not complete."
    break
}

Improvements

With all good scripts, there is always room for improvement. The one that is blaring is the waiting for the install. This should really be a loop checking files or a registry key. If the file or key is not present, then continue to way 30 seconds. This would speed up the process as some computers process faster while others do not.

The second is the get-ciminstance because this is a slow command. We can improve the speed of this command by targeting once again either a file or a registry key instead. This way we can prove it was installed without the 30 to 60 second wait for the get-ciminstance to do its thing.

As always, If you have any questions, feel free to ask.