PS2EXE and the Current Working Directory

I tend to use a great tool called PS2EXE to compile my PowerShell scripts into executables.  And sometimes I need to put some dependency DLLs in the same folder as my compiled exe, and reference them using:

Add-Type -Path [PathToDll]

To add the reference to the DLLs that reside in my executable folder, I need to somehow deduce the current ‘working’ directory.

This is easy to do when just running a PS1 script directly – I won’t bore you with that.  But the trouble with wrapping your PowerShell scripts inside an executable is that the ‘typical’ methods of obtaining the working/script directory don’t work.  They either return the wrong path (“c:\windows\system32”, “c:\windows\system32\WindowsPowerShell\v1.0\”, “c:\windows\temp” etc) or return no path at all!

There are a few reasons for this.  One could be the context you are running it from (i.e, you’re not double-clicking it but instead running it from the ‘Run’ key in the registry with no context). And also because perhaps the compiled executable runs your ‘wrapped’ PowerShell script in-memory, or it puts a copy in the %temp% location and runs it from there (hence it doesn’t matter where the executable physically exists on disk).

Coupled with this, some methods of obtaining the script/exe directory wouldn’t work in either the PowerShell console, the PowerShell ISE, or the compiled executable!  And I wanted one approach that worked for all.

The only method that worked for me (and I stole this code from here) was:

$currentDirectory = [System.AppDomain]::CurrentDomain.BaseDirectory.TrimEnd('\') 
if ($currentDirectory -eq $PSHOME.TrimEnd('\')) 
{     
	$currentDirectory = $PSScriptRoot 
}

List Certificates for Local Machine and Current User

This is a quick PowerShell script that I knocked up to list all certificates and their location (local machine or current user and the certificate store).  You can tweak it where required.

get-childitem -Path Cert:\ | foreach-object ({
    $location = $_.Location
    foreach($store in $_.StoreNames.Keys) {         
        get-childitem -Path "Cert:\$location\$store" | foreach-object ({
            $thumb = $($_.ThumbPrint)
            $issuer = $($_.Issuer)          
            write-host "$location $store $issuer"
        })
    }
}) 

Find Membership Count of all AD Groups in OU

I was working on an application migration recently, and wanted to see how many users/computers were in each application Active Directory (AD) group in a specified Organisational Unit (OU):

$Groups = Get-ADGroup -Properties * -Filter * -SearchBase "OU=Applications,DC=alkane,DC=co,DC=uk" 
Foreach($group In $groups)
{
    $groupname = $group.Name 
    $groupdescription = $group.Description
    $groupdn = $group.DistinguishedName
    $groupcount = $group.member.count
}

 

Charting with PowerShell

This post is heavily plagiarised from here.  But sometimes I see an interesting nugget of code and like to make sure I never lose it!

I’ve made a few tweaks from the aforementioned article, but I could see me using this one day to run reports on a scheduled task (connecting to SQL server etc) and emailing some pretty charts to management teams.  Here’s the code:

# load the appropriate assemblies 
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") 
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")

#create form
$Form = New-Object Windows.Forms.Form 
$Form.Text = "PowerShell Chart" 
$Form.Width = 600 
$Form.Height = 600 
$Form.controls.add($Chart) 

#create chart  
$Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart 
$Chart.Width = 500 
$Chart.Height = 400 
$Chart.Left = 40 
$Chart.Top = 30

#define font (otherwise when we export the chart as an image, the default text isn't legible)
$font = new-object system.drawing.font("calibri",12,[system.drawing.fontstyle]::Regular)

#create a chartarea to draw on and add to chart 
$ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea 
$Chart.ChartAreas.Add($ChartArea)

#add data to chart 
$Cities = @{London=7556900; Berlin=3429900; Madrid=3213271; Rome=2726539; Paris=2188500} 
[void]$Chart.Series.Add("Data") 
$Chart.Series["Data"].Points.DataBindXY($Cities.Keys, $Cities.Values)

#display the chart on a form 
$Chart.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor 
                [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left 

#add title and axes labels 
[void]$Chart.Titles.Add("Top 5 European Cities by Population") 
$ChartArea.AxisX.Title = "European Cities" 
$ChartArea.AxisY.Title = "Population"

#find point with max/min values and change their colour 
$maxValuePoint = $Chart.Series["Data"].Points.FindMaxByValue() 
$maxValuePoint.Color = [System.Drawing.Color]::Red
$minValuePoint = $Chart.Series["Data"].Points.FindMinByValue() 
$minValuePoint.Color = [System.Drawing.Color]::Green

#change chart area colour 
$Chart.BackColor = [System.Drawing.Color]::Transparent

#make bars into 3d cylinders 
$Chart.Series["Data"]["DrawingStyle"] = "Cylinder"

#define fonts for chart
$Chart.chartAreas[0].AxisX.LabelStyle.Font = $font
$Chart.chartAreas[0].AxisY.LabelStyle.Font = $font
$Chart.Titles[0].font = $font
$ChartArea.AxisX.Titlefont = $font
$ChartArea.AxisY.Titlefont = $font

#add a save button 
$SaveButton = New-Object Windows.Forms.Button 
$SaveButton.Text = "Save" 
$SaveButton.Top = 500 
$SaveButton.Left = 450 
$SaveButton.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right 
$SaveButton.add_click({$Chart.SaveImage("c:\temp\Chart.png", "PNG")})
$Form.controls.add($SaveButton)

#save chart to file 
$Chart.SaveImage($Env:USERPROFILE + "\Desktop\Chart.png", "PNG")

#show form
$Form.Add_Shown({$Form.Activate()}) 
$Form.ShowDialog()

 

PowerShell, Selenium and Browser Automation

Selenium is a portable software-testing framework for web applications.  It’s pretty cool (in a geeky way).  It’s primarily used to test web applications, but in this instance we’re using it to launch Internet Explorer, load a web-based helpdesk dashboard on the intranet, log into it and click a few buttons to customise the view.

You’ll need to download IEDriverServer.exe.  Go to this location:

http://selenium-release.storage.googleapis.com/index.html

Choose the folder for the most recent version, and download IEDriverServer_Win32_[version].zip

Then download Selenium.WebDriver Nuget package from here: https://www.nuget.org/packages/Selenium.WebDriver/

and Selenium.Support Nuget package from here: https://www.nuget.org/packages/Selenium.Support/

Rename the extensions of each file from .nupkg to .zip, and extract them as you would normally.  If you then look in the Lib folder of each archive you will find:

WebDriver.dll and WebDriver.Support.dll.

An Example of Browser Automation

Thi example demonstrates Internet Explorer browser automation since it’s probably the most prevalent browser in corporate environments.  However Selenium does have libraries for Firefox and Chrome too.

So for this example, put IEDriverServer.exe, WebDriver.dll, WebDriver.Support.dll and this PS1 file all in the same folder.  Read the inline comments in the script for an explanation.  To get the website element IDs you’ll need to use the DOM explorer of the browser, but I won’t go into detail of how to do that here.

#add references to the Selenium DLLs 
$WebDriverPath = Resolve-Path "$PSScriptRoot\WebDriver.dll"
#I unblock it because when you download a DLL from a remote source it is often blocked by default
Unblock-File $WebDriverPath
Add-Type -Path $WebDriverPath

$WebDriverSupportPath = Resolve-Path "$PSScriptRoot\WebDriver.Support.dll"
Unblock-File $WebDriverSupportPath
Add-Type -Path $WebDriverSupportPath

#before we start, we must ensure all zones are running either in protected mode, or not.  They need to all be the same.
#(we might be able to negate the requirement for some of these using InternetExplorerOptions.IntroduceInstabilityByIgnoringProtectedModeSettings)

#set protected
#local
New-ItemProperty "hkcu:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0" -Name "2500" -Value 0 -PropertyType DWORD -Force | Out-Null
#internet
New-ItemProperty "hkcu:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1" -Name "2500" -Value 0 -PropertyType DWORD -Force | Out-Null
#intranet
New-ItemProperty "hkcu:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\2" -Name "2500" -Value 0 -PropertyType DWORD -Force | Out-Null
#trusted
New-ItemProperty "hkcu:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3" -Name "2500" -Value 0 -PropertyType DWORD -Force | Out-Null
#restricted
New-ItemProperty "hkcu:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\4" -Name "2500" -Value 0 -PropertyType DWORD -Force | Out-Null

#fix to run in kiosk mode (to use ForceCreateProcessApi this must be 0. ForceCreateProcessApi is required to use BrowserCommandLineArguments)
New-ItemProperty "hkcu:\Software\Microsoft\Internet Explorer\Main" -Name "TabProcGrowth" -Value 0 -PropertyType DWORD -Force | Out-Null

#Set zoom 100%.  Again, we can probably use InternetExplorerOptions.IgnoreZoomSetting as an alternative
New-ItemProperty "hkcu:\Software\Microsoft\Internet Explorer\Zoom" -Name "ZoomFactor" -Value 100000 -PropertyType DWORD -Force | Out-Null

#can pass this stuff in when we instantiate driver if needs be (if we want a chromeless browser for example)
$seleniumOptions = New-Object OpenQA.Selenium.IE.InternetExplorerOptions
#open this URL when Internet Explorer launches
$seleniumOptions.InitialBrowserUrl = "https://localhost:8080";
#we require this option to run in kiosk mode
$seleniumOptions.ForceCreateProcessApi = $true
#open Internet Explorer in kiosk mode
$seleniumOptions.BrowserCommandLineArguments = "-k"
#untested - ignore zoom options, negating the registry fix above
#$seleniumOptions.IgnoreZoomSetting = $true

#now we create a default service so we can run Selenium without the black debug command prompt appearing
#pre-PowerShell 5 we can do it like so
New-Variable -Name IEDS -Value ([OpenQA.Selenium.IE.InternetExplorerDriverService]) -Force
$defaultservice = $IEDS::CreateDefaultService()

#PowerShell 5 we can do it like so
#$defaultservice = [OpenQA.Selenium.IE.InternetExplorerDriverService]::CreateDefaultService()

#hide command prompt
$defaultservice.HideCommandPromptWindow = $true;

#provide our default service and selenium options to the Internett Explorer driver (calling this opens the IE session)
$seleniumDriver = New-Object OpenQA.Selenium.IE.InternetExplorerDriver -ArgumentList @($defaultservice, $seleniumOptions)

#now we start clicking elements on the web page.  We do this by finding the ID of the element we want to interact with.

#enter a username into login prompt
$seleniumWait = New-Object -TypeName OpenQA.Selenium.Support.UI.WebDriverWait($seleniumDriver, (New-TimeSpan -Seconds 10))
$seleniumWait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementIsVisible([OpenQA.Selenium.By]::Id("textfield-1011-inputEl")))
$seleniumDriver.FindElementById("username_text").SendKeys("exampleusernname")

#enter a password into login prompt
$seleniumWait = New-Object -TypeName OpenQA.Selenium.Support.UI.WebDriverWait($seleniumDriver, (New-TimeSpan -Seconds 10))
$seleniumWait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementIsVisible([OpenQA.Selenium.By]::Id("textfield-1012-inputEl")))
$seleniumDriver.FindElementById("password_text").SendKeys("examplepassword")

#click 'login' button
$seleniumWait = New-Object -TypeName OpenQA.Selenium.Support.UI.WebDriverWait($seleniumDriver, (New-TimeSpan -Seconds 10))
$seleniumWait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementIsVisible([OpenQA.Selenium.By]::Id("loginButton")))
$seleniumDriver.FindElementById("loginButton").Click()

#when logged in, click another button
$seleniumWait = New-Object -TypeName OpenQA.Selenium.Support.UI.WebDriverWait($seleniumDriver, (New-TimeSpan -Seconds 10))
$seleniumWait.Until([OpenQA.Selenium.Support.UI.ExpectedConditions]::ElementIsVisible([OpenQA.Selenium.By]::Id("button-1025")))
$seleniumDriver.FindElementById("random_button").Click()

#we don't close it in this instance because we want to keep the browser open as a dashboard view
#$seleniumDriver.Close()
#$seleniumDriver.Dispose()
#$seleniumDriver.Quit()

 

Invoke-Sqlcmd returning unwanted columns such as RowError, HasErrors

I’ve been performing some SQL queries recently using PowerShell and Invoke-SqlCmd.  Here is a simple example of returning a list of devices (a single column of data) from a database:

Invoke-Sqlcmd -ServerInstance "AlkaneSQLInstance" -Database "AlkaneSQLDatabase" -Query "SELECT Device FROM Devices"

Of course I pipe this into Export-CSV like so:

Invoke-Sqlcmd -ServerInstance "AlkaneSQLInstance" -Database "AlkaneSQLDatabase" -Query "SELECT Device FROM Devices" | Export-Csv -NoTypeInformation -Path "C:\Temp\Alkane.csv"

When we read the exported data in the CSV we expect one column of data called ‘Device’.  However, instead we can also see columns called RowError, HasErrors and others!  And whilst I’m not sure why these appear, we can omit them like so:

Invoke-Sqlcmd -ServerInstance "AlkaneSQLInstance" -Database "AlkaneSQLDatabase" -Query "SELECT Device FROM Devices" | Select * -ExcludeProperty RowError, RowState, Table, ItemArray, HasErrors

and of course if we wanted to pipe this into a CSV we can do so like this:

Invoke-Sqlcmd -ServerInstance "AlkaneSQLInstance" -Database "AlkaneSQLDatabase" -Query "SELECT Device FROM Devices" | Select * -ExcludeProperty RowError, RowState, Table, ItemArray, HasErrors | Export-Csv -NoTypeInformation -Path "C:\Temp\Alkane.csv"

And voila.  We only get the columns of data that we asked for!

Manipulate Column Data with Select-Object and PowerShell

A couple of days ago I wanted to check which products were installed on a computer using PowerShell.  And throughout my journey I discovered how we can dynamically maniuplate column data and format the output.  Take this basic example where we query the ‘Uninstall’ registry key and output the registry data for displayname, displayversion, installdate:

gci -recurse HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall | get-itemproperty | sort displayname | select-object displayname, displayversion, installdate

But what if we wanted to join/concatenate the displayname and displayversion columns into one column?  Or what if we wanted to give the column a custom name?  We can do so like his:

gci -recurse HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall | get-itemproperty | sort displayname | select-object @{Label='Name and Version'; Expression={$_.displayname + " and the version is " + $_.displayversion}}, installdate

So we’ve joined the displayname and displayversion columns into one column called ‘Name and Version’.  Great.  Now by default the installdate registry data is stored in the yyyyMMdd format, or,  as an example 20180807.

This isn’t very readable.  So we can also manipulate this format (ok, we don’t do any casting as a date in this example – just basic string manipulation) and call the column ‘Formatted Date’ like so:

gci -recurse HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall | get-itemproperty | sort displayname | select-object @{Label='Name and Version'; Expression={$_.displayname + " " + $_.displayversion}}, @{Label='Formatted Date'; Expression={$_.installdate.substring(6,2) + "/" + $_.installdate.substring(4,2) + "/" + $_.installdate.substring(2,2)}}

This was quite a crude example, but hopefully it demonstrates how we can manipulate column data dynamically using PowerShell.

Move or Copy AD users

This is a simple PowerShell script to either copy AD users or move AD users from one AD group to another:

Move AD Users

$sourceADGroupName = "example 1"
$targetADGroupName = "example 2"

Get-ADGroupMember $sourceADGroupName | ForEach-Object {
  Add-ADGroupMember -Identity $targetADGroupName -Members $_ -Confirm:$false
  Remove-ADGroupMember -Identity $sourceADGroupName -Members $_ -Confirm:$false
}

Copy AD Users

$sourceADGroupName = "example 1"
$targetADGroupName = "example 2"

Get-ADGroupMember $sourceADGroupName | ForEach-Object {
  Add-ADGroupMember -Identity $targetADGroupName -Members $_ -Confirm:$false
}

 

Install and Uninstall an MSI using PowerShell

This is an example of how to install and uninstall an MSI using PowerShell.

It passes in a string array as the msiexec arguments. So you can add more arguments as you see fit. Pay close attention to the quotes around the file paths (in case they contain spaces).

$MSIInstallArguments = @(
    "/i"
    '"c:\alkane.msi"'
    "/qb!"
    "/norestart"
    "/l*v"
    '"C:\Temp\alkane_install_log.log"'
)
Start-Process "msiexec.exe" -ArgumentList $MSIInstallArguments -Wait -NoNewWindow 


$MSIUninstallArguments = @(
    "/x"
    "{0233CEF0-B5CD-40BB-AEAD-A131A547112E}"
    "/qb!"
    "/norestart"
    "/l*v"
    '"C:\Temp\alkane_uninstall_log.log"'
)
Start-Process "msiexec.exe" -ArgumentList $MSIUninstallArguments -Wait -NoNewWindow

 

Using a Hashtable for Key/Value pairs

This is a simple example of how we can use a hashtable to store and update key/value pairs:

#create hashtable
$states = @{}

#add a key/value pair
$states.Add("ExampleKey", "ExampleValue1")

#two ways of getting the value
write-host $states.Get_Item("ExampleKey")
write-host $states.ExampleKey

#update the value for the key 'ExampleKey'
$states.Set_Item("ExampleKey", "ExampleValue2")

#two ways of getting the value
write-host $states.Get_Item("ExampleKey")
write-host $states.ExampleKey