Resolve a Site name to Geo Location

Resolve a Site name to Geo Location

With everything that happened with Facebook yesterday, I began to wonder where does my query goes when I type in facebook.com. So, I did a few things and found out. The first thing I did was resolve the name facebook.com to an IP address, or group of IP addresses in this case with the command resolve-dnsname.

Resolve-DnsName -Name facebook.com

Then from there, I used the site, ip-api.com to pull the location information of the IP address. This awesome little site gives you city, state, country, zip codes, and even the ISP information of an IP address.

$Info = Invoke-RestMethod -Method Get -URI "http://ip-api.com/json/$IP"

That’s the base of the code that we will explore. It’s very straightforward, but I want to clean it up some. I want to make a Get GEO IP information and a Resolve DNSname to Geo IP. I want it to all work together even if there is multiple IP addresses and hosts names. So, lets start off with the scripts and break them down. This will contain two functions for what we are wanting.

Get-SHDGeoIP

function Get-SHDGeoIP {
    [cmdletbinding()]
    param (
        [parameter(Mandatory = $true)][ipaddress[]]$IPAddress,
        [switch]$Complete
    )
    foreach ($IP in $IPAddress) {
        
        $Info = Invoke-RestMethod -Method Get -URI "http://ip-api.com/json/$IP"
        if ($Complete) {
            $Info
        }
        else {
            [pscustomobject]@{
                IPAddress = $info.Query
                City      = $Info.city
                State     = $Info.regionName
                Country   = $Info.country
                ISP       = $Info.isp
            }
        }
    }
}

This script is going to pull the geo information for us. We start off with the parameters. We are testing the parameters to see if the IP address is an valid IP address. We do that with [ipaddress]. This tests for both IPv4 and IPv6. We tell it to be a array of IPaddresses with the [] inside of it. [ipaddress[]]. Just for cleaner fun, I have a switch for a complete information dump. This way

Since this is an array of IP addresses, we will start a foreach loop for each IP address in the array. We start the foreach loop by grabbing the IP information. If the user selected complete, we just dump the information we gathered to the user. if they didn’t select complete, we create a custom object with the IP address, city, state, country and ISP information.

Resolve-SHDDNSNameToGeoIP

Function Resolve-SHDDNSNameToGeoIP {
    [cmdletbinding()]
    param (
        [parameter(Mandatory = $true)][string[]]$Hostname,
        [switch]$Complete
    )
    foreach ($Name in $Hostname) {
        if ($Complete) {
            Get-SHDGeoIP -IPAddress (Resolve-DnsName -Name $Name).IPAddress -Complete
        }
        else { 
            Get-SHDGeoIP -IPAddress (Resolve-DnsName -Name $Name).IPAddress
        }
    }
}

The next function uses the previous function and combines it with Resolve-DnsName. We start off with a list of strings for our hostname parameter and our complete parameter. We start our loop like before of the host names. Then we use the Get-SHDGeoIP -IPAddress command with the Resolve-DnsName -Name and the link name. We then select the IP addresses which is an array. We place that array inside the Get-SHDGeoIP and bam, we have our information. Converting a hostname like Facebook.com to IP information.

With these two little scripts, you will be able to find quick information about a website and where it is being hosted. For example, this site is hosted in new jersey. I personally didn’t know that.

Let me know if you use this and how.

Datamuse API and PowerShell

Datamuse API and PowerShell

There are 171,476 or so words in the English language. This doesn’t include names or other items. Datamuse is a very powerful database of English words. It contains rhyming, like, anonymous, Synonyms and much more. What I like about this site is it has a simple API that can be stacked. You can find the API here.

What do I mean by stacked. Some APIs use the URL and gathers all the information from that. While others uses the custom header and body of requests. Datamuse uses the URL. It returns a nice json to work with as well inside its content area. Basically, it’s my kind of API. The below API is an example of a url API. After words? each XX= has the word to query with. Each item means something different. The ML means, means like and the sp is spelled like. So we are looking for words that means like black and starts with the letter b.

https://api.datamuse.com/words?ml=Black&sp=b*

URL API’s are super easy to work within PowerShell. The key is to make a string of what you are looking for. If you have a flag of $meansLike = Black. Then it will add the &ml=black to the string. Same as $Spelling=”B*”. It will add the &ml=Black&sp=B*. Then we just remove the first character of the string and we have our string. We do that with substring. Let’s take a look at the full script.

The Script

function Get-SHDWords {
    [cmdletbinding()]
    param (
        [string]$MeansLike,
        [string]$SoundsLike,
        [string]$SpelledLike,
        [string]$LeftContext,
        [string]$RightContext,
        [string]$Topic,
        [string]$NounModifiedByAdjective,
        [string]$AdjectiveModifiedbyNoun,
        [string]$Synonym,
        [string]$Trigger,
        [string]$Antonym,
        [string]$KindOf,
        [string]$MoreGeneralThan,
        [string]$Comprises,
        [string]$Meronym,
        [string]$FrequentFollowers,
        [string]$FrequentPredecessors,
        [string]$Rhymes,
        [string]$NearRhymes,
        [string]$Homophones,
        [string]$Consonant,
        [switch]$Random
    )
    $actionstring = ""
    if ($PSBoundParameters.ContainsKey('MeansLike')) { $actionstring = "$($actionstring)&ml=$MeansLike" }
    if ($PSBoundParameters.ContainsKey('SoundsLike')) { $actionstring = "$($actionstring)&sl=$SoundsLike" }
    if ($PSBoundParameters.ContainsKey('SpelledLike')) { $actionstring = "$($actionstring)&sp=$SpelledLike" }
    if ($PSBoundParameters.ContainsKey('LeftContext')) { $actionstring = "$($actionstring)&lc=$LeftContext" }
    if ($PSBoundParameters.ContainsKey('RightContext')) { $actionstring = "$($actionstring)&rc=$RightContext" }
    if ($PSBoundParameters.ContainsKey('Topic')) { $actionstring = "$($actionstring)&topics=$Topic" }
    if ($PSBoundParameters.ContainsKey('NounModifiedByAdjective')) { $actionstring = "$($actionstring)&rel_jja=$NounModifiedByAdjective" }
    if ($PSBoundParameters.ContainsKey('AdjectiveModifiedbyNoun')) { $actionstring = "$($actionstring)&rel_jjb=$AdjectiveModifiedbyNoun" }
    if ($PSBoundParameters.ContainsKey('Synonym')) { $actionstring = "$($actionstring)&rel_syn=$Synonym" }
    if ($PSBoundParameters.ContainsKey('Trigger')) { $actionstring = "$($actionstring)&rel_trg=$Trigger" }
    if ($PSBoundParameters.ContainsKey('Antonym')) { $actionstring = "$($actionstring)&rel_ant=$Antonym" }
    if ($PSBoundParameters.ContainsKey('KindOf')) { $actionstring = "$($actionstring)&rel_spc=$KindOf" }
    if ($PSBoundParameters.ContainsKey('MoreGeneralThan')) { $actionstring = "$($actionstring)&rel_gen=$MoreGeneralThan" }
    if ($PSBoundParameters.ContainsKey('Comprises')) { $actionstring = "$($actionstring)&rel_com=$Comprises" }
    if ($PSBoundParameters.ContainsKey('Meronym')) { $actionstring = "$($actionstring)&rel_par=$Meronym" }
    if ($PSBoundParameters.ContainsKey('FrequentFollowers')) { $actionstring = "$($actionstring)&rel_bag=$FrequentFollowers" }
    if ($PSBoundParameters.ContainsKey('FrequentPredecessors')) { $actionstring = "$($actionstring)&rel_bgb=$FrequentPredecessors" }
    if ($PSBoundParameters.ContainsKey('Rhymes')) { $actionstring = "$($actionstring)&rel_rhy=$Rhymes" }
    if ($PSBoundParameters.ContainsKey('NearRhymes')) { $actionstring = "$($actionstring)&rel_nry=$NearRhymes" }
    if ($PSBoundParameters.ContainsKey('Homophones')) { $actionstring = "$($actionstring)&rel_hom=$Homophones" }
    if ($PSBoundParameters.ContainsKey('Consonant')) { $actionstring = "$($actionstring)&rel_cns=$Consonant" }
    if ($Random) {
        $Character = [char](Get-Random -Minimum 65 -Maximum 90)
        $actionstring = "$($actionstring)&sp=$Character*"
    }
    $actionstring = $actionstring.Substring(1)
        (Invoke-WebRequest -Uri "https://api.datamuse.com/words?$actionstring").content | convertfrom-json
}

The Breakdown

Ok, first notice that we have each of the items from the datamuse site as string values that can take input. None of them are mandatory. The first thing after the parameters is our blank action string.

Next, we start questioning the input like a youtube conspiracy theorist. We do that by asking if the bound parameters contains a key. If it does we add to the action string.

    if ($PSBoundParameters.ContainsKey('MeansLike')) { $actionstring = "$($actionstring)&ml=$MeansLike" }

Here we are recreating the string by taking the value of the string and adding the &ml=$MeansLike. We do this for each parameter we use except for random. Random we just grab a random english word character and ask for a word that starts with that word. After we have created our action string we use our substring command to remove the first & from the string.

$actionstring = $actionstring.Substring(1)

Once we have the action string formatted right, we are ready to go. We use the invoke-webrequest command and we will grab the data. We will select only the content, which is in a nice json format. Then we will pipe that information into convertfrom-json to create a Powershell object.

(Invoke-WebRequest -Uri "https://api.datamuse.com/words?$actionstring").content | convertfrom-json

From here you can select the data you want. Lets look at some examples.

Example 1 – Link

Get-SHDWords -SpelledLike "L*" -Rhymes "Pink"

Here are the results:

word     score numSyllables
----     ----- ------------
link      2267            1
linc       144            1
lip sync   105            2
lynk        43            1
linke       31            1
linck        2            1

Here we are looking for all the words that start with the letter L and that Rhyme with Pink.

Example 2 – Lash

Get-SHDWords -MeansLike anger -SoundsLike love | Where-Object {$_.tags -contains "v"}

Here are the results:

word score tags
---- ----- ----
lash 14742 {v}

In this example, we are looking for words that mean anger but sound like love. Some of the results will have tags. Items like means like and sounds like have tags that you can filter through. With this example, we are looking for verbs. So we look with the where-object command.

Example 3 – Random Word

$Test = Get-SHDWords -Random
$test[(Get-Random -Minimum 0 -Maximum $test.count)].word
ominous

In this example, we get a random list of words that start with a random letter. Then we select from that list a random item. We do that with the get-random with a minimum of 0 and the maximum of the test count. Then we select the word. All that produced the word ominous.

Taking a step farther – Passwords

Let’s take this one step further and make a two-word password. The first thing we want is a word. So we use the command Get-SHDwords again. We are going to use the word trust.

$Charcter = [char](Get-Random -Minimum 33 -Maximum 64)
$CharcterEnd = [char](Get-Random -Minimum 33 -Maximum 64)
$CharcterMiddle = [char](Get-Random -Minimum 33 -Maximum 64)
$One = "Trust"
$Two = Get-SHDWords -Rhymes $One
$Two = $two[(Get-Random -Minimum 0 -Maximum $two.count)].word -replace ("[ aAeEiIoOuUyY](?!.*[aAeEiIoOuUyY])", "$Charcter")
"$one$($CharcterMiddle)$Two$($CharcterEnd)"

The password this produced was: Trust<comb?st’

Here we generate 3 random characters. We then select our word, trust by putting it into the $One. We can replace our word by replacing the $One input. Then we get the second word with two. Get-SHDWords -Rhymes $One Then we grab just one of those words like before. This time we replace all the vowels with the character we generated at the beginning. Then we combine it all together. The first word, $One, the middle character, $characterMiddle, the second word $two, and finally the ending Character $CharacterEnd. Now we have an easy-to-remember password.

Datamuse offers even more options than what this script offers. Check them out and see what amazing things you can create.

I’m going to leave off here with a challenge this week. Can you write a poem using datamuse based off the word:

$Test = Get-SHDWords -Random
$test[(Get-Random -Minimum 0 -Maximum $test.count)].word

Post your results in the comments.

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)
    }
}
Log Public IP Changes

Log Public IP Changes

This little script logs public IP address changes. The original design was to email out. I’ll make sure to point out when and where to put that code. So the idea is simple. It checks to see if it’s public IP address has changed using an invoke-webrequest. If it has changed, it checks to see if a log has been made. If it has been made, it updates the log, if it hasn’t been made, it creates the log. Then when the IP address changes back, it backs up the logs and stops. Lets take a look at the script.

The Script

Param (
    [string]$IPAddress = "Your IP Address"
)  
$IP = (invoke-webrequest -Uri "http://ifconfig.me/ip" -UseBasicParsing).content
If (!(Test-Path C:\temp)) { New-Item -Path "c:\temp" -ItemType Directory }
if (!($IP -like "$IPAddress")) {
    if (!(Test-Path c:\temp\IPTrigger.log )) {

#Email Code Goes Here

        $Datetime = (Get-Date).ToString("yyyy-MM-dd_HH-mm")
        "Datetime,IP" > c:\temp\IPTrigger.log
        "$datetime,$IP" >> c:\temp\IPTrigger.log 
    }
    else {
        $Datetime = (Get-Date).ToString("yyyy-MM-dd_HH-mm")
        "$datetime,$IP" >> c:\temp\IPTrigger.log 
    }
}
else {
    if (Test-Path c:\temp\IPTrigger.log) {
        $Datetime = (Get-Date).ToString("yyyy-MM-dd_HH-mm")
        Copy-Item -Path c:\temp\IPTrigger.log -Destination "c:\temp\IPTrigger_$($datetime).log.bak"
        Remove-Item -Path  c:\temp\IPTrigger.log

#Email Code goes here with attachment of the log bak. 

    }
}

The Breakdown

We grab the IP address from the Paramters. This way you can have the script called Public-IPChecker -IPAddress Something from your task scheduler. After we get what the IP Address should be we get what It is with the Invoke-webrequest command.

$IP = (invoke-webrequest -Uri "http://ifconfig.me/ip" -UseBasicParsing).content

There are a hand full of sites that grabs the IP address for you. I personally like Ifconfig.me because it places the IP address as the content. Thus no sorting through the HTML to get the IP address.

Next we ask if it’s the IP address. Then we ask if the log has been made.

if (!($IP -like "$IPAddress")) {
    if (!(Test-Path c:\temp\IPTrigger.log )) {}
}

If it doesn’t exist, we create it by getting the DateTime into a file friendly format. and Piping that information into the log file along with the current IP address.

$Datetime = (Get-Date).ToString("yyyy-MM-dd_HH-mm")
"Datetime,IP" > c:\temp\IPTrigger.log
"$datetime,$IP" >> c:\temp\IPTrigger.log 

If it does exist, we just update the log file using the same code but this time we remove the “Datetime,IP” part.

$Datetime = (Get-Date).ToString("yyyy-MM-dd_HH-mm")
"$datetime,$IP" >> c:\temp\IPTrigger.log 

If the IP addresses do match then we test to see if the log file is there. If it is there we create a bak of the log file and remove the old one. This is when you can send an email saying it’s back up. If it doesn’t exist, we do nothing.

if (Test-Path c:\temp\IPTrigger.log) {
        $Datetime = (Get-Date).ToString("yyyy-MM-dd_HH-mm")
        Copy-Item -Path c:\temp\IPTrigger.log -Destination "c:\temp\IPTrigger_$($datetime).log.bak"
        Remove-Item -Path  c:\temp\IPTrigger.log
}

That’s the full script in a nutshell.

Quser to PSObject

Quser to PSObject

I have been using Quser for years and I was wondering how to parse the data out to Powershell. The best way to do this is to convert the output into a csv and then convert it from the csv to a psobject. Let us get started. First we output the quser to a variable.

$Users = Quser /server:"$($env:COMPUTERNAME)" 2> $null

In this example, we are going to grab the local computer’s logged-in information. Here is what the output looks like:

PS C:\Users\david> Quser /server:"$($env:COMPUTERNAME)" 2> $null
 USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
 david                 console            11  Active       1:05  9/5/2021 9:29 PM
 susan                                    12  Disc         1:05  9/6/2021 11:14 AM

Next, we replace the first (^) > with nothing. It doesn’t look like much but it adds the spacing needed.

$Users = $Users -Replace '^>',''

Here is the output.

 USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
 david                 console            11  Active       1:08  9/5/2021 9:29 PM
 susan                                    12  Disc         1:08  9/6/2021 11:14 AM

Now we will replace the tabs with commas.

$Users = $Users -Replace '\s{2,}',','
USERNAME,SESSIONNAME,ID,STATE,IDLE TIME,LOGON TIME
 david,console,11,Active,1:08,9/5/2021 9:29 PM
 susan,12,Disc,1:08,9/6/2021 11:14 AM

From here we want to go through each line and remove the last item if it has too many spaces. This happens when you run this command on a server. You will need to go through each line by line with a foreach loop.

$users = foreach ($User in $Users) {
    if ($User.Split(',').count -eq 5) {
        Write-Output ($User -replace '(^[^,]+)','$1,')
    } else {
        Write-Output $User
    }
}

Splitting the user by the comma, we need only 4 not 5. So we remove the first of the last object. If it is 4, we do nothing special and just reintroduce the original $user.

USERNAME,SESSIONNAME,ID,STATE,IDLE TIME,LOGON TIME
 david,console,11,Active,1:08,9/5/2021 9:29 PM
 susan,,12,Disc,1:08,9/6/2021 11:14 AM

Now we take that $user csv and convert it to a psobject using convertfrom-csv.

$users = $users | ConvertFrom-Csv
USERNAME    : david
SESSIONNAME : console
ID          : 11
STATE       : Active
IDLE TIME   : 1:08
LOGON TIME  : 9/5/2021 9:29 PM

USERNAME    : susan
SESSIONNAME :
ID          : 12
STATE       : Disc
IDLE TIME   : .
LOGON TIME  : 9/6/2021 11:14 AM

Next, we need to do some house cleaning. Sometimes the Idle time will show up as a dot. We don’t want that. We want it to show up as null. We do this by finding all the dot Idle times and setting that idle time to null with a foreach-object.

$users | where-object { $_."IDLE TIME" -like "." } | ForEach-Object { $_."IDLE TIME" = $null }
USERNAME    : david
SESSIONNAME : console
ID          : 11
STATE       : Active
IDLE TIME   : 1:08
LOGON TIME  : 9/5/2021 9:29 PM

USERNAME    : susan
SESSIONNAME :
ID          : 12
STATE       : Disc
IDLE TIME   :
LOGON TIME  : 9/6/2021 11:14 AM

Finally, we output the information by just putting the $users. That’s it yall. Lets put it together.

The Script

$Users = Quser /server:"$($env:COMPUTERNAME)" 2> $null
$Users = $Users -Replace '^>',''
$Users = $Users -Replace '\s{2,}',','
$users = foreach ($User in $Users) {
    if ($User.Split(',').count -eq 5) {
        Write-Output ($User -replace '(^[^,]+)','$1,')
    } else {
        Write-Output $User
    }
} 
$users = $users | ConvertFrom-Csv
$users | where-object { $_."IDLE TIME" -like "." } | ForEach-Object { $_."IDLE TIME" = $null }
$Users

Another Way

Another way to do this is to grab a process from the machine that everyone logged in would be using. Something like explorer. We do this with the get-ciminstance and the wmi object win32_process.

Get-ciminstance -ClassName win32_process -filter 'name ="explorer.exe"'
ProcessId Name         HandleCount WorkingSetSize VirtualSize
--------- ----         ----------- -------------- -----------
10576     explorer.exe 2805        142168064      2204137693184
18900     explorer.exe 2404        185618432      2204239441920

Then we pipe this information into Invoke-CimMethod and use the GetOwner method to grab the owner information.

Get-ciminstance -ClassName win32_process -filter 'name ="explorer.exe"' | Invoke-CimMethod -MethodName GetOwner

Domain          ReturnValue User  PSComputerName
------          ----------- ----  --------------
DESKTOP-XXXX0LB           0 david
                          2    

From here we grab the user information using a .user.

(Get-ciminstance -ClassName win32_process -filter 'name ="explorer.exe"' | Invoke-CimMethod -MethodName GetOwner).user