PSSoftware.psm1

Set-StrictMode -Version Latest

function Test-InstalledSoftware
{
    <#
    .SYNOPSIS
        This function is used as a quick check to see if a specific software product is installed on the local host.
    .PARAMETER Name
         The name of the software you'd like to query as displayed by the Get-InstalledSoftware function
    .PARAMETER Version
        The version of the software you'd like to query as displayed by the Get-InstalledSofware function.
    .PARAMETER Guid
        The GUID of the installed software
    #>

    [OutputType([bool])]
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [string]$Version,
        
        [Parameter(ParameterSetName = 'Guid')]
        [ValidateNotNullOrEmpty()]
        [Alias('ProductCode')]
        [string]$Guid
    )
    process
    {
        try
        {
            
            if ($PSBoundParameters.ContainsKey('Name'))
            {
                if ($PSBoundParameters.ContainsKey('Version'))
                {
                    $SoftwareInstances = Get-InstalledSoftware -Name $Name | Where-Object { $_.Version -eq $Version }
                }
                else
                {
                    $SoftwareInstances = Get-InstalledSoftware -Name $Name
                }
            }
            elseif ($PSBoundParameters.ContainsKey('Guid'))
            {
                $SoftwareInstances = Get-InstalledSoftware -Guid $Guid
            }
            
            
            if (-not $SoftwareInstances)
            {
                Write-Log -Message 'The software is NOT installed.'
                $false
            }
            else
            {
                Write-Log -Message 'The software IS installed.'
                $true
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-InstalledSoftware
{
    <#
    .SYNOPSIS
        Retrieves a list of all software installed
    .EXAMPLE
        Get-InstalledSoftware
         
        This example retrieves all software installed on the local computer
    .PARAMETER Name
        The software title you'd like to limit the query to.
    .PARAMETER Guid
        The software GUID you'e like to limit the query to
    #>

    [OutputType([System.Management.Automation.PSObject])]
    [CmdletBinding()]
    param (
        [string]$Name,
        
        [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
        [string]$Guid
    )
    process
    {
        try
        {
            
            $UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
            New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null
            $UninstallKeys += Get-ChildItem HKU: -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" }
            if (-not $UninstallKeys)
            {
                Write-Log -Message 'No software registry keys found' -LogLevel '2'
            }
            else
            {
                foreach ($UninstallKey in $UninstallKeys)
                {
                    $friendlyNames = @{
                        'DisplayName' = 'Name'
                        'DisplayVersion' = 'Version'
                    }
                    if ($PSBoundParameters.ContainsKey('Name'))
                    {
                        $WhereBlock = { $_.GetValue('DisplayName') -like "$Name*" }
                    }
                    elseif ($PSBoundParameters.ContainsKey('GUID'))
                    {
                        $WhereBlock = { $_.PsChildName -eq $Guid }
                    }
                    else
                    {
                        $WhereBlock = { $_.GetValue('DisplayName') }
                    }
                    $SwKeys = Get-ChildItem -Path $UninstallKey -ErrorAction SilentlyContinue | Where-Object $WhereBlock
                    foreach ($SwKey in $SwKeys)
                    {
                        try {
                            $output = @{ }
                            foreach ($ValName in $SwKey.GetValueNames() | Where-Object { $_ })
                            {
                                if ($ValName -ne 'Version')
                                {
                                    Write-Verbose -Message $ValName
                                    $output.InstallLocation = ''
                                    if ($ValName -eq 'InstallLocation' -and ($SwKey.GetValue($ValName)) -and (@('C:', 'C:\Windows', 'C:\Windows\System32', 'C:\Windows\SysWOW64') -notcontains $SwKey.GetValue($ValName).TrimEnd('\')))
                                    {
                                        $output.InstallLocation = $SwKey.GetValue($ValName).TrimEnd('\')
                                    }
                                    [string]$ValData = $SwKey.GetValue($ValName)
                                    if ($friendlyNames[$ValName])
                                    {
                                        $output[$friendlyNames[$ValName]] = $ValData.Trim() ## Some registry values have trailing spaces.
                                    }
                                    else
                                    {
                                        $output[$ValName] = $ValData.Trim() ## Some registry values trailing spaces
                                    }
                                }
                            }
                            $output.GUID = ''
                            if ($SwKey.PSChildName -match '\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')
                            {
                                $output.GUID = $SwKey.PSChildName
                            }
                            New-Object â€“TypeName PSObject -Property $output
                        } catch {
                            Write-Log -Message $_.Exception.Message -LogLevel '2'
                        }
                    }
                }
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Install-Software
{
    <#
    .SYNOPSIS
 
    .NOTES
        Created on: 6/23/2014
        Created by: Adam Bertram
        Filename: Install-Software.ps1
        Credits:
        Requirements: The installers executed via this script typically need "Run As Administrator"
        Todos: Allow multiple software products to be installed
    .EXAMPLE
        Install-Software -MsiInstallerFilePath install.msi -InstallArgs "/qn "
    .PARAMETER InstallShieldInstallerFilePath
         This is the file path to the EXE InstallShield installer.
    .PARAMETER MsiInstallerFilePath
         This is the file path to the MSI installer.
    .PARAMETER OtherInstallerFilePath
         This is the file path to any other EXE installer.
    .PARAMETER MsiExecSwitches
        This is a string of arguments that are passed to the installer. If this param is
        not used, it will default to the standard REBOOT=ReallySuppress and the ALLUSERS=1 switches. If it's
        populated, it will be concatenated with the standard silent arguments. Use the -Verbose switch to discover arguments used.
        Do NOT use this to pass TRANSFORMS or PATCH arguments. Use the MstFilePath and MspFilePath params for that.
    .PARAMETER MstFilePath
        Use this param if you've created a TRANSFORMS file and would like to pass this to the installer
    .PARAMETER MspFilePath
        Use this param if you have a patch to apply to the install
    .PARAMETER InstallShieldInstallArgs
        This is a string of arguments that are passed to the InstallShield installer. Default arguments are
        "/s /f1$IssFilePath /SMS"
    .PARAMETER OtherInstallerArgs
        This is a string of arguments that are passed to any other EXE installer. There is no default.
    .PARAMETER KillProcess
        A list of process names that will be terminated prior to attempting the install. This is useful
        in upgrade scenarios where you need to terminate the previous version's processes.
    .PARAMETER ProcessTimeout
        A value (in seconds) that the installer script will wait for the installation process to complete. If the installation
        goes over this value, any processes (parent or child) will be terminated.
    .PARAMETER LogFilePath
        This is the path where the installer log file will be written. If not passed, it will default
        to being named install.log in the system temp folder.
 
    #>

    [OutputType([void])]
    [CmdletBinding(DefaultParameterSetName = 'MSI')]
    param (
        [Parameter(ParameterSetName = 'InstallShield', Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [ValidatePattern('\.exe$')]
        [ValidateNotNullOrEmpty()]
        [string]$InstallShieldInstallerFilePath,
        
        [Parameter(ParameterSetName = 'Other', Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [ValidatePattern('\.exe$')]
        [ValidateNotNullOrEmpty()]
        [string]$OtherInstallerFilePath,
        
        [Parameter(ParameterSetName = 'InstallShield', Mandatory = $true)]
        [ValidatePattern('\.iss$')]
        [ValidateNotNullOrEmpty()]
        [string]$IssFilePath,
        
        [Parameter(ParameterSetName = 'MSI', Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [ValidateNotNullOrEmpty()]
        [string]$MsiInstallerFilePath,
        
        [Parameter(ParameterSetName = 'MSI')]
        [ValidateNotNullOrEmpty()]
        [string]$MsiExecSwitches,
        
        [Parameter(ParameterSetName = 'MSI')]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [ValidatePattern('\.msp$')]
        [ValidateNotNullOrEmpty()]
        [string]$MspFilePath,
        
        [Parameter(ParameterSetName = 'MSI')]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [ValidatePattern('\.mst$')]
        [ValidateNotNullOrEmpty()]
        [string[]]$MstFilePath,
        
        [Parameter(ParameterSetName = 'InstallShield')]
        [ValidateNotNullOrEmpty()]
        [string]$InstallShieldInstallArgs,
        
        [Parameter(ParameterSetName = 'Other')]
        [ValidateNotNullOrEmpty()]
        [Alias('OtherInstallerArguments')]
        [string]$OtherInstallerArgs,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$KillProcess,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$ProcessTimeout = 600,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$LogFilePath
    )
    
    process
    {
        try
        {
            
            
            ## Common Start-Process parameters across all installers. We'll add to this hashtable as we go
            $ProcessParams = @{
                'NoNewWindow' = $true;
                'Passthru' = $true
            }
            
            
            if ($PSBoundParameters.ContainsKey('MsiInstallerFilePath'))
            {
                $InstallerFilePath = $MsiInstallerFilePath
                Write-Log -Message 'Creating the msiexec install string'
                
                $InstallArgs = Get-MsiexecInstallString -InstallerFilePath $InstallerFilePath -MspFilePath $MspFilePath -MstFilePath $MstFilePath -LogFilePath $LogFilePath -ExtraSwitches $MsiExecSwitches
                
                ## Add Start-Process parameters
                $ProcessParams['FilePath'] = 'msiexec.exe'
                $ProcessParams['ArgumentList'] = $InstallArgs
            }
            elseif ($PSBoundParameters.ContainsKey('InstallShieldInstallerFilePath'))
            {
                $InstallerFilePath = $InstallShieldInstallerFilePath
                
                $InstallArgs = Get-InstallshieldInstallString -InstallerFilePath $InstallerFilePath -LogFilePath $LogFilePath -ExtraSwitches $InstallShieldInstallArgs -IssFilePath $IssFilePath
                
                $ProcessParams['FilePath'] = $InstallerFilePath
                $ProcessParams['ArgumentList'] = $InstallArgs
            }
            elseif ($PSBoundParameters.ContainsKey('OtherInstallerFilePath'))
            {
                $InstallerFilePath = $OtherInstallerFilePath
                Write-Log -Message 'Creating a generic setup install string'
                
                ## Nothing fancy here. Since we don't know any common switches to run I'll just take whatever
                ## arguments are provided as a parameter.
                if ($PSBoundParameters.ContainsKey('OtherInstallerArgs'))
                {
                    $ProcessParams['ArgumentList'] = $OtherInstallerArgs
                }
                $ProcessParams['FilePath'] = $OtherInstallerFilePath
                
            }
            
            ## Thiw was added for upgrade scenarios where the previous version would be running and the installer
            ## itself isn't smart enough to kill it.
            if ($PSBoundParameters.ContainsKey('KillProcess'))
            {
                Write-Log -Message 'Killing existing processes'
                $KillProcess | ForEach-Object { Stop-MyProcess -ProcessName $_ }
            }
            
            Write-Log -Message "Starting the command line process `"$($ProcessParams['FilePath'])`" $($ProcessParams['ArgumentList'])..."
            $Result = Start-Process @ProcessParams
            
            ## This is required because sometimes depending on how the MSI is packaged, the parent process will exit
            ## but will leave child processes running and the function will exit before the install is finished.
            if ($PSBoundParameters.ContainsKey('MsiInstallerFilePath'))
            {
                Wait-WindowsInstaller
            }
            else
            {
                ## No special msiexec.exe waiting here. We'll just use Wait-MyProcess to report on the waiting
                ## process.
                Write-Log "Waiting for process ID $($Result.Id)"
                
                $WaitParams = @{
                    'ProcessId' = $Result.Id
                    'ProcessTimeout' = $ProcessTimeout
                }
                Wait-MyProcess @WaitParams
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Remove-Software
{
    <#
    .SYNOPSIS
        This function removes any software registered via Windows Installer from the local computer
    .NOTES
        Created on: 6/4/2014
        Created by: Adam Bertram
        Requirements: The msizap utility (if user would like to run)
    .DESCRIPTION
        This function searches a local computer for a specified application matching a name. Based on the
        parameters given, it can either remove services, kill proceseses and if the software is
        installed, it uses the locally cached MSI to initiate an uninstall and has the option to
        ensure the software is completely removed by running the msizap.exe utility.
    .EXAMPLE
        Remove-Software -Name 'Adobe Reader' -KillProcess 'proc1','proc2'
        This example would remove any software with 'Adobe Reader' in the name and look for and stop both the proc1
        and proc2 processes
    .EXAMPLE
        Remove-Software -Name 'Adobe Reader'
        This example would remove any software with 'Adobe Reader' in the name.
    .EXAMPLE
        Remove-Software -Name 'Adobe Reader' -RemoveService 'servicename' -Verbose
        This example would remove any software with 'Adobe Reader' in the name, look for, stop and remove any service with a
        name of servicename. It will output all verbose logging as well.
    .EXAMPLE
        Remove-Software -Name 'Adobe Reader' -RemoveFolder 'C:\Program Files Files\Install Folder'
        This example would remove any software with 'Adobe Reader' in the name, look for and remove the
        C:\Program Files\Install Folder, attempt to uninstall the software cleanly via msiexec using
        the syntax msiexec.exe /x PRODUCTMSI /qn REBOOT=ReallySuppress which would attempt to not force a reboot if needed.
        If it doesn't uninstall cleanly, it would run copy the msizap utility from the default path to
        the local computer, execute it with the syntax msizap.exe TW! PRODUCTGUID and remove itself when done.
    .PARAMETER Name
        This is the name of the application to search for. This can be multiple products. Each product will be removed in the
        order you specify.
    .PARAMETER MsiExecSwitches
        Specify a string of switches you'd like msiexec.exe to run when it attempts to uninstall the software. By default,
        it already uses "/x GUID /qn". You can specify any additional parameters here.
    .PARAMETER LogFilePath
        The file path where the msiexec uninstall log will be created. This defaults to the name of the product being
        uninstalled in the system temp directory
    .PARAMETER InstallshieldLogFilePath
        The file path where the Installshield log will be created. This defaults to the name of the product being
        uninstalled in the system temp directory
    .PARAMETER RunMsizap
        Use this parameter to run the msizap.exe utility to cleanup any lingering remnants of the software
    .PARAMETER MsizapParams
        Specify the parameters to send to msizap if it is needed to cleanup the software on the remote computer. This
        defaults to "TWG!" which removes settings from all user profiles
    .PARAMETER MsizapFilePath
        Optionally specify where the file msizap utility is located in order to run a final cleanup
    .PARAMETER IssFilePath
        If removing an InstallShield application, use this parameter to specify the ISS file path where you recorded
        the uninstall of the application.
    .PARAMETER InstallShieldSetupFilePath
        If removing an InstallShield application, use this optional paramter to specify where the EXE installer is for
        the application you're removing. This is only used if no cached installer is found.
    #>

    [OutputType([void])]
    [CmdletBinding(DefaultParameterSetName = 'None')]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'FromPipeline')]
        [ValidateNotNullOrEmpty()]
        [object]$Software,
    
        [Parameter(Mandatory = $true,
             ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'FromValue')]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'MSI')]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [string]$MsiExecSwitches,
        
        [Parameter()]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [string]$LogFilePath,
        
        [Parameter(ParameterSetName = 'ISS')]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [string]$InstallshieldLogFilePath,
        
        [Parameter(ParameterSetName = 'Msizap')]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [switch]$RunMsizap,
        
        [Parameter(ParameterSetName = 'Msizap')]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [string]$MsizapParams = 'TWG!',
        
        [Parameter(ParameterSetName = 'Msizap')]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string]$MsizapFilePath = 'C:\MyDeployment\msizap.exe',
        
        [Parameter(ParameterSetName = 'ISS',
             Mandatory = $true)]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [ValidatePattern('\.iss$')]
        [string]$IssFilePath,
        
        [Parameter(ParameterSetName = 'ISS')]
        [Parameter(ParameterSetName = 'FromPipeline')]
        [Parameter(ParameterSetName = 'FromValue')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string]$InstallShieldSetupFilePath
    )
    process
    {
        try
        {
            if ($PSCmdlet.ParameterSetName -eq 'FromValue')
            {
                Write-Debug -Message "Getting installed software matching [$($Name)]"
                $Software = Get-InstalledSoftware -Name $Name    
            }
            if ($Software.InstallLocation)
            {
                Write-Log -Message "Stopping all processes under the install folder $($Software.InstallLocation)..."
                Stop-SoftwareProcess -Software $Software
            }
            
            if ($Software.UninstallString)
            {
                $InstallerType = Get-InstallerType $Software.UninstallString
            }
            else
            {
                Write-Log -Message "Uninstall string for $($Software.Name) not found" -LogLevel '2'
            }
            if (-not $PsBoundParameters['LogFilePath'])
            {
                $script:LogFilePath = "$(Get-SystemTempFolderPath)\$($Software.Name).log"
                Write-Log -Message "No log file path specified. Defaulting to $script:LogFilePath..."
            }
            if (-not $InstallerType -or ($InstallerType -eq 'Windows Installer'))
            {
                Write-Log -Message "Installer type detected to be Windows Installer or unknown for $($Software.Name). Attempting Windows Installer removal" -LogLevel '2'
                $params = @{ }
                if ($PSBoundParameters.ContainsKey('MsiExecSwitches'))
                {
                    $params.MsiExecSwitches = $MsiExecSwitches
                }
                if ($Software.GUID)
                {
                    $params.Guid = $Software.GUID
                }
                else
                {
                    $params.Name = $Software.Name
                }
                
                Uninstall-WindowsInstallerPackage @params
                
            }
            elseif ($InstallerType -eq 'InstallShield')
            {
                Write-Log -Message "Installer type detected as Installshield."
                $Params = @{
                    'IssFilePath' = $IssFilePath;
                    'Name' = $Software.Name
                    'SetupFilePath' = $InstallShieldSetupFilePath
                }
                if ($InstallshieldLogFilePath)
                {
                    $Params.InstallshieldLogFilePath = $InstallshieldLogFilePath
                }
                Uninstall-InstallShieldPackage @Params
            }
            if (Test-InstalledSoftware -Name $Software.Name)
            {
                Write-Log -Message "$($Software.Name) was not uninstalled via traditional uninstall" -LogLevel '2'
                if ($RunMsizap.IsPresent)
                {
                    Write-Log -Message "Attempting Msizap..."
                    Uninstall-ViaMsizap -Guid $Software.GUID -MsizapFilePath $MsizapFilePath -Params $MsiZapParams
                }
                else
                {
                    Write-Log -Message "$($Software.Name) failed to uninstall successfully" -LogLevel '3'
                }
            }

            $outputProps = @{ }
            if (-not (Test-InstalledSoftware -Name $Software.Name))
            {
                Write-Log -Message "Successfully removed $($Software.Name)"
            }
            else
            {
                throw "Failed to remove $($Software.Name)"
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Import-Certificate
{
    <#
    .SYNOPSIS
        This function imports a certificate into any certificate store on a local computer
    .EXAMPLE
        PS> Import-Certificate -Context LocalMachine -StoreName My -FilePath C:\certificate.cer
 
        This example will import the certificate.cert certificate into the Personal store for the
        local computer
    .EXAMPLE
        PS> Import-Certificate -Context CurrentUser -StoreName TrustedPublisher -FilePath C:\certificate.cer
 
        This example will import the certificate.cer certificate into the Trusted Publishers store for the
        currently logged on user
    .PARAMETER Context
         This is the Context (either CurrentUser or LocalMachine) where the store is located which the certificate
        will go into.
    .PARAMETER StoreName
        This is the certificate store that the certificate will be placed into
    .PARAMETER FilePath
        This is the path to the certificate file that you'd like to import
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Context,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({
            if ($Context -eq 'CurrentUser')
            {
                (Get-ChildItem Cert:\CurrentUser | Select-Object -ExpandProperty name) -contains $_
            }
            else
            {
                (Get-ChildItem Cert:\LocalMachine | Select-Object -ExpandProperty name) -contains $_
            }
        })]
        [string]$StoreName,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$FilePath
    )
    
    begin
    {
        $ErrorActionPreference = 'Stop'
        try
        {
            [void][System.Reflection.Assembly]::LoadWithPartialName('System.Security')
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    
    process
    {
        try
        {
            $Cert = Get-Item $FilePath
            $Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Cert
            $X509Store = New-Object System.Security.Cryptography.X509Certificates.X509Store $StoreName, $Context
            $X509Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
            $X509Store.Add($Cert)
            $X509Store.Close()
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Test-Process
{
    <#
    .SYNOPSIS
        This function is called after the execution of an external CMD process to log the status of how the process was exited.
    .PARAMETER Process
        A System.Diagnostics.Process object type that is output by using the -Passthru parameter on the Start-Process cmdlet
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [System.Diagnostics.Process]$Process
    )
    process
    {
        try
        {
            
            if (@(0, 3010) -notcontains $Process.ExitCode)
            {
                Write-Log -Message "Process ID $($Process.Id) failed. Return value was $($Process.ExitCode)" -LogLevel '2'
                $false
            }
            else
            {
                Write-Log -Message "Process ID $($Process.Id) exited with successfull exit code '$($Process.ExitCode)'."
                $true
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-ChildProcess
{
    <#
    .SYNOPSIS
        This function childs all child processes a parent process has spawned
    .PARAMETER ProcessId
        The potential parent process ID
    #>

    [OutputType([System.Management.ManagementObject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ProcessId
    )
    process
    {
        try
        {
            Get-WmiObject -Class Win32_Process -Filter "ParentProcessId = '$ProcessId'"
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Stop-MyProcess
{
    <#
    .SYNOPSIS
        This function stops a process while provided robust logging of the activity
    .PARAMETER ProcessName
        One more process names
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$ProcessName
    )
    process
    {
        try
        {
            
            $ProcessesToStop = Get-Process -Name $ProcessName -ErrorAction 'SilentlyContinue'
            if (-not $ProcessesToStop)
            {
                Write-Log -Message "-No processes to be killed found..."
            }
            else
            {
                foreach ($process in $ProcessesToStop)
                {
                    Write-Log -Message "-Process $($process.Name) is running. Attempting to stop..."
                    $WmiProcess = Get-WmiObject -Class Win32_Process -Filter "name='$($process.Name).exe'" -ErrorAction 'SilentlyContinue' -ErrorVariable WMIError
                    if ($WmiError)
                    {
                        throw "process $($process.Name). WMI query errored with `"$($WmiError.Exception.Message)`""
                    }
                    elseif ($WmiProcess)
                    {
                        foreach ($p in $WmiProcess)
                        {
                            if ($PSCmdlet.ShouldProcess("Process ID: $($p.Id)", 'Stop'))
                            {
                                $WmiResult = $p.Terminate()
                                if ($WmiResult.ReturnValue -eq 1603)
                                {
                                    Write-Log -Message "Process $($p.name) exited successfully but needs a reboot."
                                }
                                elseif ($WmiResult.ReturnValue -ne 0)
                                {
                                    throw "-Unable to stop process $($p.name). Return value was $($WmiResult.ReturnValue)"
                                }
                                else
                                {
                                    Write-Log -Message "-Successfully stopped process $($p.Name)..."
                                }
                            }
                        }
                    }
                }
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Stop-SoftwareProcess
{
    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [object]$Software
    )
    begin {
        $ErrorActionPreference = 'Stop'
    }
    process {
        try
        {
            $Processes = (Get-Process | Where-Object { $_.Path -like "$($Software.InstallLocation)*" } | Select-Object -ExpandProperty Name)
            if ($Processes)
            {
                Write-Log -Message "Sending processes: $Processes to Stop-MyProcess..."
                ## Check to see if the process is still running. It's possible the termination of other processes
                ## already killed this one.
                $Processes = $Processes | Where-Object { Get-Process -Name $_ -ErrorAction 'SilentlyContinue' }
                if ($PSCmdlet.ShouldProcess("Process ID $($Processes)", 'Stop'))
                {
                    Stop-MyProcess $Processes
                }
            }
            else
            {
                Write-Log -Message 'No processes running under the install folder path'
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Wait-MyProcess
{
    <#
    .SYNOPSIS
        This function waits for a process and waits for all that process's children before releasing control
    .PARAMETER ProcessId
        A process Id
    .PARAMETER ProcessTimeout
        An interval (in seconds) to wait for the process to finish. If the process hasn't exited within this timeout
        it will be terminated. The default is 600 seconds (5 minutes) so no process will run longer than that.
    .PARAMETER ReportInterval
        The number of seconds between when it is logged that the process is still pending
 
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [int]$ProcessId,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$ProcessTimeout = 600,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$ReportInterval = 15
        
    )
    process
    {
        try
        {
            Write-Log -Message "Finding the process ID '$ProcessId'..."
            $Process = Get-Process -Id $ProcessId -ErrorAction 'SilentlyContinue'
            if ($Process)
            {
                Write-Log -Message "Process '$($Process.Name)' ($($Process.Id)) found. Waiting to finish and capturing all child processes."

                ## While waiting for the initial process to stop, collect all child IDs it spawns
                $ChildProcessesToLive = @()
                
                ## Start the timer to ensure we have a point to get total time from
                $Timer = [Diagnostics.Stopwatch]::StartNew()
                $i = 0
                
                ## Do this while the parent process is still running
                while (-not $Process.HasExited)
                {
                    ## Find any and all child processes the parent process spawned
                    $ChildProcesses = Get-ChildProcess -ProcessId $ProcessId
                    if ($ChildProcesses)
                    {
                        Write-Log -Message "Found [$(@($ChildProcesses).Count)] child process(es)"
                        ## If any child processes are found, collect them all
                        $ChildProcessesToLive += $ChildProcesses
                    }
                    if ($Timer.Elapsed.TotalSeconds -ge $ProcessTimeout)
                    {
                        Write-Log -Message "The process '$($Process.Name)' ($($Process.Id)) has exceeded the timeout of $ProcessTimeout seconds. Killing process."
                        $Timer.Stop()
                        Stop-MyProcess -ProcessName $Process.Name
                    }
                    elseif (($i % $ReportInterval) -eq 0) ## Use a modulus here to write to the log every X seconds
                    {
                        Write-Log "Still waiting for process '$($Process.Name)' ($($Process.Id)) after $([Math]::Round($Timer.Elapsed.TotalSeconds, 0)) seconds"
                    }
                    Start-Sleep -Milliseconds 100
                    $i++
                }

                Write-Log "Process '$($Process.Name)' ($($Process.Id)) has finished after $([Math]::Round($Timer.Elapsed.TotalSeconds, 0)) seconds"
                if ($ChildProcessesToLive) ## If any child processes were spawned while the parent process was running
                {
                    $ChildProcessesToLive = $ChildProcessesToLive | Select-Object -Unique ## Ensure we didn't accidently capture duplicate PIDs
                    Write-Log -Message "Parent process '$($Process.Name)' ($($Process.Id)) has finished but still has $(@($ChildProcessesToLive).Count) child processes ($($ChildProcessesToLive.Name -join ',')) left. Waiting on these to finish."
                    foreach ($Process in $ChildProcessesToLive)
                    {
                        Wait-MyProcess -ProcessId $Process.ProcessId
                    }
                }
                else
                {
                    Write-Log -Message 'No child processes found spawned'
                }
                Write-Log -Message "Finished waiting for process '$($Process.Name)' and all child processes"
            }
            else
            {
                Write-Log -Message "Process ID '$ProcessId' not found. No need to wait on it."
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-InstallshieldInstallString
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$InstallerFilePath,
    
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$IssFilePath,
    
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$LogFilePath,
    
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ExtraSwitches
    )
    begin {
        $ErrorActionPreference = 'Stop'
    }
    process {
        try
        {
            Write-Log -Message 'Creating the InstallShield setup install string'
            
            ## We're adding common InstallShield switches here. -s is silent, -f1 specifies where the
            ## ISS file we createed previously lives, -f2 specifies a log file location and /SMS is a special
            ## switch that prevents the setup.exe was exiting prematurely.
            if (-not $PSBoundParameters.ContainsKey('LogFilePath'))
            {
                $LogFilePath = "$(Get-SystemTempFolderPath)\$($InstallerFilePath | Split-Path -Leaf).log"
            }
            if (-not $ExtraSwitches)
            {
                $InstallArgs = "-s -f1`"$IssFilePath`" -f2`"$LogFilePath`" /SMS"
            }
            else
            {
                $InstallArgs = "-s -f1`"$IssFilePath`" $ExtraSwitches -f2`"$LogFilePath`" /SMS"
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Uninstall-InstallShieldPackage
{
    <#
    .SYNOPSIS
        This function runs an uninstall for any InstallShield packaged software. This function utilitizes an
        InstallShield ISS file to silently uninstall the application.
    .PARAMETER Name
        One or more software titles of the InstallShield package you'd like to uninstall.
    .PARAMETER IssFilePath
        The file path where the pre-built silent answer file (ISS) is located.
    .PARAMETER SetupFilePath
        The file path where the EXE InstallShield installer is located.
    .PARAMETER LogFilePath
        The log file path where the InstallShield installer will log results. If not log file path
        is specified it will be created in the system temp folder.
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$Name,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [string]$IssFilePath,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [string]$SetupFilePath,
        
        [ValidateScript({ Test-Path -Path ($_ | Split-Path -Parent) -PathType 'Container' })]
        [string]$LogFilePath = "(Get-SystemTempFolderPath)\IssSetupLog.log"
    )
    process
    {
        try
        {
            
            foreach ($Product in $Name)
            {
                Write-Log -Message "Beginning uninstall for Installshield product '$Name'"
                ## Find the uninstall string to find the cached setup.exe
                $Products = Get-InstalledSoftware $Product
                ## If multiple products are found, remove them all
                foreach ($p in $Products)
                {
                    $UninstallString = $p.UninstallString
                    ## Check to ensure anything is in the UninstallString property
                    if (-not $p.UninstallString)
                    {
                        Write-Log -Message "No uninstall string found for product $Title" -LogLevel '2'
                    }
                    elseif ($p.UninstallString -match '(\w:\\[a-zA-Z0-9 _.() { }-]+\\.*.exe)+')
                    {
                        ## Test to ensure the cached setup.exe exists
                        if (-not (Test-Path $Matches[0]))
                        {
                            Write-Log -Message "Installer file path not found in $($p.UninstallString) or cannot be found on the file system" -LogLevel '2'
                        }
                        else
                        {
                            $InstallerFilePath = $Matches[0]
                            Write-Log -Message "Valid installer file path is $InstallerFilePath"
                        }
                    }
                    if (-not $InstallerFilePath)
                    {
                        if (-not $SetupFilePath)
                        {
                            Write-Log -Message "No setup folder path specified. This software cannot be removed" -LogLevel '2'
                            continue
                        }
                        else
                        {
                            $InstallerFilePath = $SetupFilePath
                        }
                    }
                    ## Run the setup.exe passing the ISS file to uninstall
                    if ($InstallshieldLogFilePath)
                    {
                        $MyLogFilePath = $InstallshieldLogFilePath
                    }
                    else
                    {
                        $MyLogFilePath = $script:LogFilePath
                    }
                    $InstallArgs = "/s /f1`"$IssFilePath`" /f2`"$MyLogFilePath`" /SMS"
                    Write-Log -Message "Running the install syntax `"$InstallerFilePath`" $InstallArgs"
                    $Process = Start-Process "`"$InstallerFilePath`"" -ArgumentList $InstallArgs -Wait -NoNewWindow -PassThru
                    if (-not (Test-InstalledSoftware $Title))
                    {
                        Write-Log -Message "The product $Title was successfully removed!"
                    }
                    else
                    {
                        Write-Log -Message "The product $Title was not removed. Attempting secondary uninstall method" -LogLevel '2'
                        ## Parse out the EXE file path and arguments. This regex could be improved on big time.
                        $FilePathRegex = '(([a-zA-Z]\:|\\)\\([^\\]+\\)*[^\/:*?"<>|]+\.[a-zA-Z]{3})" (.+)'
                        if ($UninstallString -match $FilePathRegex)
                        {
                            $InstallerFilePath = $matches[1]
                            $InstallArgs = $matches[4]
                            $InstallArgs = "$InstallArgs /s /f1`"$IssFilePath`" /f2`"$MyLogFilePath`" /SMS"
                            Write-Log -Message "Running the install syntax `"$InstallerFilePath`" $InstallArgs"
                            $Process = Start-Process "`"$InstallerFilePath`"" -ArgumentList $InstallArgs -Wait -NoNewWindow -PassThru
                            if (-not (Test-InstalledSoftware $Title))
                            {
                                throw "The product '$Title' was not removed!"
                            }
                        }
                        else
                        {
                            throw "Could not parse out the setup installer and arguments from uninstall string. The product '$Title' was not removed!"
                        }
                    }
                }
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Start-Log
{
    <#
    .SYNOPSIS
        This function creates the initial log file and sets a few global variables
        that are common among the session. Call this function at the very top of your
        installer script.
 
    .PARAMETER FilePath
        The file path where you'd like to place the log file on the file system. If no file path
        specified, it will create a file in the system's temp directory named the same as the script
        which called this function with a .log extension.
 
    .EXAMPLE
        PS C:\> Start-Log -FilePath 'C:\Temp\installer.log
 
    .NOTES
 
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [ValidateScript({ Split-Path $_ -Parent | Test-Path })]
        [string]$FilePath = "$(Get-SystemTempFolderPath)\PSSoftware.log"
    )
    
    try
    {
        if (-not (Test-Path $FilePath))
        {
            ## Create the log file
            New-Item $FilePath -ItemType File | Out-Null
        }
        
        ## Set the global variable to be used as the FilePath for all subsequent Write-Log
        ## calls in this session
        $global:ScriptLogFilePath = $FilePath
    }
    catch
    {
        Write-Error $_.Exception.Message
    }
}

function Write-Log
{
    <#
    .SYNOPSIS
        This function creates or appends a line to a log file
 
    .DESCRIPTION
        This function writes a log line to a log file in the form synonymous with
        ConfigMgr logs so that tools such as CMtrace and SMStrace can easily parse
        the log file. It uses the ConfigMgr client log format's file section
        to add the line of the script in which it was called.
 
    .PARAMETER Message
        The message parameter is the log message you'd like to record to the log file
 
    .PARAMETER LogLevel
        The logging level is the severity rating for the message you're recording. Like ConfigMgr
        clients, you have 3 severity levels available; 1, 2 and 3 from informational messages
        for FYI to critical messages that stop the install. This defaults to 1.
 
    .EXAMPLE
        PS C:\> Write-Log -Message 'Value1' -LogLevel 'Value2'
        This example shows how to call the Write-Log function with named parameters.
 
    .NOTES
 
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message,
        
        [Parameter()]
        [ValidateSet(1, 2, 3)]
        [int]$LogLevel = 1
    )
    Set-StrictMode -Off
    try
    {
        $TimeGenerated = "$(Get-Date -Format HH:mm:ss).$((Get-Date).Millisecond)+000"
        ## Build the line which will be recorded to the log file
        $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">'
        $LineFormat = $Message, $TimeGenerated, (Get-Date -Format MM-dd-yyyy), "$($MyInvocation.ScriptName | Split-Path -Leaf):$($MyInvocation.ScriptLineNumber)", $LogLevel
        $Line = $Line -f $LineFormat
        
        if (-not (Test-Path Variable:\ScriptLogFilePath))
        {
            Write-Verbose -Message $Message
        }
        else
        {
            Add-Content -Value $Line -Path $ScriptLogFilePath
        }
    }
    catch
    {
        Write-Error $_.Exception.Message
    }
}

function Convert-CompressedGuidToGuid
{
    <#
    .SYNOPSIS
        This converts a compressed GUID also known as a product code into a GUID.
    .DESCRIPTION
        This function will typically be used to figure out the MSI installer GUID
        that matches up with the product code stored in the 'SOFTWARE\Classes\Installer\Products'
        registry path.
    .EXAMPLE
        Convert-CompressedGuidToGuid -CompressedGuid '2820F6C7DCD308A459CABB92E828C144'
     
        This example would output the GUID '{7C6F0282-3DCD-4A80-95AC-BB298E821C44}'
    .PARAMETER CompressedGuid
        The compressed GUID you'd like to convert.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [ValidatePattern('^[0-9a-fA-F]{32}$')]
        [string]$CompressedGuid
    )
    process
    {
        try
        {
            $Indexes = [ordered]@{
                0 = 8;
                8 = 4;
                12 = 4;
                16 = 2;
                18 = 2;
                20 = 2;
                22 = 2;
                24 = 2;
                26 = 2;
                28 = 2;
                30 = 2
            }
            $Guid = '{'
            foreach ($index in $Indexes.GetEnumerator())
            {
                $part = $CompressedGuid.Substring($index.Key, $index.Value).ToCharArray()
                [array]::Reverse($part)
                $Guid += $part -join ''
            }
            $Guid = $Guid.Insert(9, '-').Insert(14, '-').Insert(19, '-').Insert(24, '-')
            $Guid + '}'
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Convert-GuidToCompressedGuid
{
    <#
    .SYNOPSIS
        This converts a GUID to a compressed GUID also known as a product code.
    .DESCRIPTION
        This function will typically be used to figure out the product code
        that matches up with the product code stored in the 'SOFTWARE\Classes\Installer\Products'
        registry path to a MSI installer GUID.
    .EXAMPLE
        Convert-GuidToCompressedGuid -Guid '{7C6F0282-3DCD-4A80-95AC-BB298E821C44}'
     
        This example would output the compressed GUID '2820F6C7DCD308A459CABB92E828C144'
    .PARAMETER Guid
        The GUID you'd like to convert.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [string]$Guid
    )
    begin
    {
        $Guid = $Guid.Replace('-', '').Replace('{', '').Replace('}', '')
    }
    process
    {
        try
        {
            
            $Groups = @(
            $Guid.Substring(0, 8).ToCharArray(),
            $Guid.Substring(8, 4).ToCharArray(),
            $Guid.Substring(12, 4).ToCharArray(),
            $Guid.Substring(16, 16).ToCharArray()
            )
            $Groups[0..2] | ForEach-Object {
                [array]::Reverse($_)
            }
            $CompressedGuid = ($Groups[0..2] | ForEach-Object { $_ -join '' }) -join ''
            
            $chararr = $Groups[3]
            for ($i = 0; $i -lt $chararr.count; $i++)
            {
                if (($i % 2) -eq 0)
                {
                    $CompressedGuid += ($chararr[$i + 1] + $chararr[$i]) -join ''
                }
            }
            $CompressedGuid
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Convert-ToUncPath
{
    <#
    .SYNOPSIS
        A simple function to convert a local file path and a computer name to a network UNC path.
    .PARAMETER LocalFilePath
        A file path ie. C:\Windows\somefile.txt
    .PARAMETER Computername
        The computer in which the file path exists on
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$LocalFilePath,
        
        [Parameter()]
        [string]$Computername
    )
    process
    {
        try
        {
            
            $RemoteFilePathDrive = ($LocalFilePath | Split-Path -Qualifier).TrimEnd(':')
            "\\$Computername\$RemoteFilePathDrive`$$($LocalFilePath | Split-Path -NoQualifier)"
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-32BitProgramFilesPath
{
    <#
    .SYNOPSIS
        On x64 machines the x86 program files path is Program Files (x86) while on x86 machines it's just Program Files. This function
        does that decision for you and just outputs the x86 program files path regardless of OS architecture
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            
            if ((Get-Architecture) -eq 'x64')
            {
                ${env:ProgramFiles(x86)}
            }
            else
            {
                $env:ProgramFiles
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-32BitRegistrySoftwarePath
{
    <#
    .SYNOPSIS
        On x64 machines the x86 Software registry key path is HKLM:\SOFTWARE\Wow6432Node while on x86 machines it's just
        HKLM:\Software. This function does that decision for you and just outputs the x86 path regardless of OS architecture.
    .PARAMETER Scope
        Specify either HKLM or HKCU. Defaults to HKLM.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [ValidateSet('HKLM', 'HKCU')]
        [string]$Scope = 'HKLM'
    )
    process
    {
        try
        {
            
            if ((Get-Architecture) -eq 'x64')
            {
                "$Scope`:\SOFTWARE\Wow6432Node"
            }
            else
            {
                "$Scope`:\SOFTWARE"
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-Architecture
{
    <#
    .SYNOPSIS
        This simple function tells you whether the machine you're running on is either x64 or x86
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            if ((Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty SystemType) -eq 'x64-based PC')
            {
                'x64'
            }
            else
            {
                'x86'
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-DriveFreeSpace
{
    <#
    .SYNOPSIS
        This finds the total hard drive free space for one or multiple hard drive partitions
    .DESCRIPTION
        This finds the total hard drive free space for one or multiple hard drive partitions. It returns free space
        rounded to the nearest SizeOutputLabel parameter
    .PARAMETER DriveLetter
        This is the drive letter of the hard drive partition you'd like to query. By default, all drive letters are queried.
    .PARAMETER SizeOutputLabel
        In what size increments you'd like the size returned (KB, MB, GB, TB). Defaults to MB.
    .PARAMETER Computername
        The computername(s) you'd like to find free space on. This defaults to the local machine.
    .EXAMPLE
        PS C:\> Get-DriveFreeSpace -DriveLetter 'C','D'
        This example retrieves the free space on the C and D drive partition.
    #>

    [CmdletBinding()]
    [OutputType([array])]
    param
    (
        [string[]]$Computername = 'localhost',
        
        [Parameter(ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true)]
        [ValidatePattern('[A-Z]')]
        [string]$DriveLetter,
        
        [ValidateSet('KB', 'MB', 'GB', 'TB')]
        [string]$SizeOutputLabel = 'MB'
        
    )
    
    Begin
    {
        try
        {
            
            $WhereQuery = "SELECT FreeSpace,DeviceID FROM Win32_Logicaldisk"
            
            if ($PsBoundParameters.DriveLetter)
            {
                $WhereQuery += ' WHERE'
                $BuiltQueryParams = { @() }.Invoke()
                foreach ($Letter in $DriveLetter)
                {
                    $BuiltQueryParams.Add("DeviceId = '$DriveLetter`:'")
                }
                $WhereQuery = "$WhereQuery $($BuiltQueryParams -join ' OR ')"
            }
            Write-Debug "Using WQL query $WhereQuery"
            $WmiParams = @{
                'Query' = $WhereQuery
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    Process
    {
        try
        {
            foreach ($Computer in $Computername)
            {
                try
                {
                    $WmiParams.Computername = $Computer
                    $WmiResult = Get-WmiObject @WmiParams
                    if (-not $WmiResult)
                    {
                        throw "Drive letter does not exist on target system"
                    }
                    foreach ($Result in $WmiResult)
                    {
                        if ($Result.Freespace)
                        {
                            [pscustomobject]@{
                                'Computername' = $Computer;
                                'DriveLetter' = $Result.DeviceID;
                                'Freespace' = [int]($Result.FreeSpace / "1$SizeOutputLabel")
                            }
                        }
                    }
                }
                catch
                {
                    throw $_
                }
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-InstallerType
{
    <#
    .SYNOPSIS
        Based on the uninstall string retrieved from the registry this function will tell you what kind of installer was
        used to install the product. This information is helpful when figuring out the best way to remove software.
     
    .PARAMETER UninstallString
        The uninstall string that's stored in the HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\%GUID% UninstallString
        registry value.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [string]$UninstallString
    )
    
    process
    {
        try
        {
            
            if ($UninstallString -imatch 'msiexec.exe')
            {
                'Windows Installer'
            }
            elseif ($UninstallString -imatch 'InstallShield Installation')
            {
                'InstallShield'
            }
            else
            {
                throw "Could not determine installer type for uninstall string [$($UninstallString)]"
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-LoggedOnUserSID
{
    <#
    .SYNOPSIS
        This function queries the registry to find the SID of the user that's currently logged onto the computer interactively.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            
            
            if (-not (Get-PSDrive -Name 'HKU' -ErrorAction SilentlyContinue))
            {
                New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null
                ## Every user that's logged on has a registry key in HKU with their SID
                Get-ChildItem HKU: | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | Select -ExpandProperty PSChildName
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-OperatingSystem
{
    <#
    .SYNOPSIS
        This function queries the operating system name from WMI.
    .DESCRIPTION
        Using a WMI query, this function uses the Win32_OperatingSystem WMI class
        to output the operating system running on $Computername
    .PARAMETER Computername
        The name of the computer to query. This defaults to the local host.
    .EXAMPLE
        PS C:\> Get-OperatingSystem -Computername MYCOMPUTER
         
        This example finds the operating system on a computer named MYCOMPUTER
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Computername = 'localhost'
    )
    process
    {
        try
        {    
            (Get-WmiObject -ComputerName $Computername -Query 'SELECT Caption FROM Win32_OperatingSystem').Caption
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-SystemTempFolderPath
{
    <#
    .SYNOPSIS
        This function uses the TEMP system environment variable to easily discover the folder path
        to the system's temp folder
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            [environment]::GetEnvironmentVariable('TEMP', 'Machine')
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-RegistryValue
{
    <#
    .SYNOPSIS
        This functions finds the typical registry value like Get-ItemProperty does but also returns
        the registry value type as well.
    .EXAMPLE
        PS> Get-MyRegistryValue -Path HKLM:\Software\7-Zip -Name 'Name'
     
        This example gets the registry data and type for the value 'Name' in the HKLM:\Software\7-Zip key
    .PARAMETER Path
         The path to the parent registry key
    .PARAMETER Name
        The name of the registry value
    #>

    [OutputType([PSObject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^\w{4}:')]
        [string]$Path,
        
        [Parameter(Mandatory = $true)]
        [string]$Name
    )
    process
    {
        try
        {
            $Key = Get-Item -Path $Path -ErrorAction 'SilentlyContinue'
            if (-not $Key)
            {
                throw "The registry key $Path does not exist"
            }
            $Value = $Key.GetValue($Name)
            if (-not $Value)
            {
                throw "The registry value $Name in the key $Path does not exist"
            }
            [pscustomobject]@{ 'Path' = $Path; 'Value' = $Value; 'Type' = $key.GetValueKind($Name) }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-AllUsersRegistryValue
{
    <#
    .SYNOPSIS
        This function finds all of the user profile registry hives, mounts them and retrieves a registry value for each user.
    .EXAMPLE
        PS> Get-AllUsersRegistryValue -RegistryInstance @{'Name' = 'Setting'; 'Path' = 'SOFTWARE\Microsoft\Windows\Something'}
     
        This example would get the string registry value 'Type' in the path 'SOFTWARE\Microsoft\Windows\Something'
        for every user registry hive.
    .PARAMETER RegistryInstance
         A hash table containing key names of 'Name' designating the registry value name and 'Path' designating the parent
        registry key the registry value is in.
    #>

    [OutputType([System.Management.Automation.PSCustomObject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable[]]$RegistryInstance
    )
    try
    {
        
        New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null
        
        ## Find the registry values for the currently logged on user
        $LoggedOnSids = Get-LoggedOnUserSID
        Write-Log -Message "Found $(@($LoggedOnSids).Count) logged on user SIDs"
        foreach ($sid in $LoggedOnSids)
        {
            Write-Log -Message "Loading the user registry hive for the logged on SID $sid"
            foreach ($instance in $RegistryInstance)
            {
                $Value = Get-ItemProperty -Path "HKU:\$sid\$($instance.Path)" -Name $instance.Name -ErrorAction SilentlyContinue
                if (-not $Value)
                {
                    Write-Log -Message "Registry value $($instance.name) does not exist in HKU:\$sid\$($instance.Path)" -LogLevel '2'
                }
                else
                {
                    $Value
                }
            }
        }

        $loggedOffUsers = Get-UserProfile -ExcludeSystemProfiles | where { $LoggedOnSids -notcontains $_.SID } | Select -ExpandProperty UserName
        
        foreach ($user in $loggedOffUsers)
        {
            try {
                Write-Log -Message "Loading the user registry hive for user $user..."
                LoadRegistryHive -Username $user
                foreach ($instance in $RegistryInstance)
                {
                    $Value = Get-ItemProperty -Path "HKU:\TempUserLoad\$($instance.Path)" -Name $instance.Name -ErrorAction SilentlyContinue
                    if (-not $Value)
                    {
                        Write-Log -Message "Registry value $($instance.name) does not exist in HKU:\TempUserLoad\$($instance.Path)" -LogLevel '2'
                    }
                    else
                    {
                        $Value
                    }
                }
                UnloadRegistryHive
            } catch {
                Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'        
            }
        }
        
    }
    catch
    {
        Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

function Get-AllUsersRegistryKey
{
    <#
    .SYNOPSIS
        This function finds all of the user profile registry hives, mounts them and retrieves a registry key for each user.
    .EXAMPLE
        PS> Get-AllUsersRegistryKey -Path 'SOFTWARE\Microsoft\Windows\Something'
     
    .PARAMETER Path
         A string representing the path to the registry key.
    #>

    [OutputType([System.Management.Automation.PSCustomObject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )
    try
    {
        
        New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null
        
        $LoggedOnSids = Get-LoggedOnUserSID
        Write-Log -Message "Found $(@($LoggedOnSids).Count) logged on user SIDs"
        foreach ($sid in $LoggedOnSids)
        {
            try {
                Write-Log -Message "Loading the user registry hive for the logged on SID $sid"
                $key = Get-Item -Path "HKU:\$sid\$Path" -ErrorAction SilentlyContinue
                if (-not $key)
                {
                    Write-Log -Message "Registry key does not exist at HKU:\$sid\$Path" -LogLevel '2'
                }
                else
                {
                    $key
                }
            } catch {
                Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'        
            } finally {
                UnloadRegistryHive
            }
        }

        $loggedOffUsers = Get-UserProfile -ExcludeSystemProfiles | where { $LoggedOnSids -notcontains $_.SID } | Select -ExpandProperty UserName
        
        foreach ($user in $loggedOffUsers)
        {
            try {
                Write-Log -Message "Loading the user registry hive for user $user..."
                LoadRegistryHive -Username $user
                $key = Get-Item -Path "HKU:\TempUserLoad\$Path" -ErrorAction SilentlyContinue
                if (-not $key)
                {
                    Write-Log -Message "Registry key does not exist at HKU:\TempUserLoad\$Path" -LogLevel '2'
                }
                else
                {
                    $key
                }
            } catch {
                Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'        
            } finally {
                UnloadRegistryHive
            }
        }
        
    }
    catch
    {
        Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

function LoadRegistryHive
{
    [OutputType([System.IO.FileInfo])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$UserName        
    )
    try {
        $regExePath = GetRegExePath
        $profilePath = Get-UserProfilePath -Username $UserName
        Write-Log -Message "Loading registry hive [$profilePath\NtUser.dat]..."
        $Process = Start-Process -FilePath $regExePath -ArgumentList "load HKU\TempUserLoad `"$profilePath\NTuser.dat`"" -Wait -NoNewWindow -PassThru
        Test-Process $Process | Out-Null
    } catch {
        Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
        $PSCmdlet.ThrowTerminatingError($_)
    }
    
}

function UnloadRegistryHive
{
    [CmdletBinding()]
    param
    ()

    $regExePath = GetRegExePath
    Write-Log -Message "Unloading HKU\TempUserLoad..."
    $Process = Start-Process -FilePath $regExePath -ArgumentList "unload HKU\TempUserLoad" -Wait -NoNewWindow -PassThru
    ## TODO This seems to work but returns 1. Commenting out for now
    # Test-Process $Process | Out-Null
}

function GetRegExePath
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    ()

    if ((Get-Architecture) -eq 'x64')
    {
        $RegPath = 'syswow64'
    }
    else
    {
        $RegPath = 'System32'
    }

    "$($env:Systemdrive)\Windows\$RegPath\reg.exe"
    
}

function Import-RegistryFile
{
    <#
    .SYNOPSIS
        A function that uses the utility reg.exe to do a bulk import of registry changes.
    .DESCRIPTION
        This function allows the user to import registry changes in bulk by means of a .reg file. This
        .reg file should only contain 1 set of registry keys such as HKLM or HKCU. If the .reg file
        contains HKLM, HKCU, HKCR or HKCC key references, the file will be imported directly with no modification. If the
        .reg file contains HKCU references, it will be modified to account for the currently logged on interactive
        user, copied to a location on the computer and will be imported under each HKCU hive when another user
        logs on.
    .PARAMETER FilePath
        The file path to the .reg file
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [string]$FilePath
    )
    begin
    {
        try
        {
            
            ## Detect if this is a registry file for HKCU, HKLM, HKU, HKCR or HKCC keys
            $Regex = 'HKEY_CURRENT_USER|HKEY_CLASSES_ROOT|HKEY_LOCAL_MACHINE|HKEY_USERS|HKEY_CURRENT_CONFIG'
            $HiveNames = Select-String -Path $FilePath -Pattern $Regex | ForEach-Object { $_.Matches.Value }
            $RegFileHive = $HiveNames | Select-Object -Unique
            if ($RegFileHive -is [array])
            {
                throw "The registry file at '$FilePath' contains more than one hive reference."
            }
            else
            {
                Write-Log -Message "Detected hive type as $RegFileHive"
            }
            if ((Get-Architecture) -eq 'x64')
            {
                $RegPath = 'syswow64'
            }
            else
            {
                $RegPath = 'System32'
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
        
    }
    process
    {
        try
        {
            if ($RegFileHive -ne 'HKEY_CURRENT_USER')
            {
                Write-Log -Message "Starting registry import of reg file $FilePath..."
                ($Result = Start-Process "$($env:Systemdrive)\Windows\$RegPath\reg.exe" -ArgumentList "import `"$FilePath`"" -Wait -NoNewWindow -PassThru) | Out-Null
                Test-Process -Process $Result
                Write-Log -Message 'Registry file import done'
            }
            else
            {
                #########
                ## Import the registry file for the currently logged on user
                #########
                $LoggedOnSids = Get-LoggedOnUserSID
                if ($LoggedOnSids.Count -gt 0)
                {
                    Write-Log -Message "Found $($LoggedOnSids.Count) logged on user SIDs"
                    foreach ($sid in $LoggedOnSids)
                    {
                        ## Replace all HKEY_CURRENT_USER references to HKCU\%SID% so that it can be applied to HKCU while not
                        ## actually running under that context. Create a new reg file with the replacements in the system's temp folder
                        $HkcuRegFilePath = "$(Get-SystemTempFolderPath)\$($FilePath | Split-Path -Leaf)"
                        Write-Log -Message "Replacing HKEY_CURRENT_USER references with HKEY_USERS\$sid and placing temp file in $HkcuRegFilePath"
                        Find-InTextFile -FilePath $FilePath -Find $RegFileHive -Replace "HKEY_USERS\$sid" -NewFilePath $HkcuRegFilePath -Force
                        
                        ## Perform a recursive function call to itself to import the newly created reg file
                        Write-Log -Message "Importing reg file $HkcuRegFilePath"
                        Import-RegistryFile -FilePath $HkcuRegFilePath
                        Write-Log -Message "Removing temporary registry file $HkcuRegFilePath"
                        Remove-Item $HkcuRegFilePath -Force
                    }
                }
                else
                {
                    Write-Log -Message 'No users currently logged on. Skipping current user registry import'
                }
                
                ########
                ## Use Active Setup to create a registry value to perform an import of the registry file for each logged on user
                ########
                Write-Log -Message "Copying $FilePath to systemp temp folder for later user"
                Copy-Item -Path $FilePath -Destination "$(Get-SystemTempFolderPath)\$($FilePath | Split-Path -Leaf)"
                Write-Log -Message "Setting Everyone full control on temp registry file so all users can import it"
                $Params = @{
                    'Path' = "$(Get-SystemTempFolderPath)\$($FilePath | Split-Path -Leaf)"
                    'Identity' = 'Everyone'
                    'Right' = 'Modify';
                    'InheritanceFlags' = 'None';
                    'PropagationFlags' = 'NoPropagateInherit';
                    'Type' = 'Allow';
                }
                Set-MyFileSystemAcl @Params
                
                Write-Log -Message "Setting registry file to import for each user"
                
                ## This isn't the *best* way to do this because this doesn't prevent a user from clearing out all the temp files
                Set-AllUserStartupAction -CommandLine "reg import `"$(Get-SystemTempFolderPath)\$($FilePath | Split-Path -Leaf)`""
                
            }
            
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Remove-RegistryKey
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Path
    )
    begin {
        $ErrorActionPreference = 'Stop'
    }
    process {
        try
        {
            foreach ($key in $Path)
            {
                if (($key | Split-Path -Qualifier) -eq 'HKLM:')
                {
                    Write-Log -Message "Removing HKLM registry key '$key' for system"
                    Remove-Item -Path $key -Recurse -Force -ErrorAction 'SilentlyContinue'
                }
                elseif (($key | Split-Path -Qualifier) -eq 'HKCU:')
                {
                    Write-Log -Message "Removing HKCU registry key '$key' for all users"
                    Set-AllUsersRegistryValue -RegistryInstance @{ 'Path' = $key.Replace('HKCU:\', '') } -Remove
                }
                else
                {
                    Write-Log -Message "Registry key '$key' not in recognized format" -LogLevel '2'
                }
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Set-AllUsersRegistryValue
{
    <#
    .SYNOPSIS
        This function sets a registry value in every user profile hive.
    .EXAMPLE
        PS> Set-AllUsersRegistryValue -RegistryInstance @{'Name' = 'Setting'; 'Type' = 'String'; 'Value' = 'someval'; 'Path' = 'SOFTWARE\Microsoft\Windows\Something'}
     
        This example would modify the string registry value 'Type' in the path 'SOFTWARE\Microsoft\Windows\Something' to 'someval'
        for every user registry hive.
    .PARAMETER RegistryInstance
         A hash table containing key names of 'Name' designating the registry value name, 'Type' to designate the type
        of registry value which can be 'String,Binary,Dword,ExpandString or MultiString', 'Value' which is the value itself of the
        registry value and 'Path' designating the parent registry key the registry value is in.
    .PARAMETER Remove
        A switch parameter that overrides the default setting to only change or add registry values. This removes one of more registry keys instead.
        If this parameter is used the only required key in the RegistryInstance parameter is Path. This will automatically remove both
        the x86 and x64 paths if the key is a child under the SOFTWARE key. There's no need to specify the WOW6432Node path also.
    .PARAMETER Force
        A switch parameter that is used if the registry value key path doesn't exist will create the entire parent/child key hierachy and creates the
        registry value. If this parameter is not used, if the key the value is supposed to be in does not exist the function will skip the value.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]$RegistryInstance,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$Remove
    )
    process
    {
        try
        {
            
            
            ## By default, the HKU provider is not added
            if (-not (Get-PSDrive -Name 'HKU' -ErrorAction SilentlyContinue))
            {
                $null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS
            }
            
            ## Change the registry values for the currently logged on user
            $LoggedOnSids = Get-LoggedOnUserSID
            Write-Log -Message "Found $(@($LoggedOnSids).Count) logged on user SIDs"
            foreach ($sid in $LoggedOnSids)
            {
                Write-Log -Message "Loading the user registry hive for the logged on SID $sid"
                foreach ($instance in $RegistryInstance)
                {
                    if ($Remove.IsPresent)
                    {
                        if ($PSCmdlet.ShouldProcess($instance.Path, 'Remove'))
                        {
                            Write-Log -Message "Removing registry key '$($instance.path)'"
                            Remove-Item -Path "HKU:\$sid\$($instance.Path)" -Recurse -Force -ErrorAction 'SilentlyContinue'
                        }
                    }
                    else
                    {
                        if (-not (Get-Item -Path "HKU:\$sid\$($instance.Path)" -ErrorAction 'SilentlyContinue'))
                        {
                            if ($PSCmdlet.ShouldProcess($instance.Path, 'New'))
                            {
                                Write-Log -Message "The registry key HKU:\$sid\$($instance.Path) does not exist. Creating..."
                                New-Item -Path "HKU:\$sid\$($instance.Path | Split-Path -Parent)" -Name ($instance.Path | Split-Path -Leaf) -Force | Out-Null
                            }
                        }
                        else
                        {
                            Write-Log -Message "The registry key HKU:\$sid\$($instance.Path) already exists. No need to create."
                        }
                        if ($PSCmdlet.ShouldProcess($instance.Path, 'New property'))
                        {
                            Write-Log -Message "Setting registry value $($instance.Name) at path HKU:\$sid\$($instance.Path) to $($instance.Value)"
                            New-ItemProperty -Path "HKU:\$sid\$($instance.Path)" -Name $instance.Name -Value $instance.Value -PropertyType $instance.Type -Force
                        }
                    }
                }
            }
            
            foreach ($instance in $RegistryInstance)
            {
                if ($Remove.IsPresent)
                {
                    if ($instance.Path.Split('\')[0] -eq 'SOFTWARE' -and ((Get-Architecture) -eq 'x64'))
                    {
                        $Split = $instance.Path.Split('\')
                        $x86Path = "HKCU\SOFTWARE\Wow6432Node\{0}" -f ($Split[1..($Split.Length)] -join '\')
                        $CommandLine = "reg delete `"{0}`" /f && reg delete `"{1}`" /f" -f "HKCU\$($instance.Path)", $x86Path
                    }
                    else
                    {
                        $CommandLine = "reg delete `"{0}`" /f" -f "HKCU\$($instance.Path)"
                    }
                }
                else
                {
                    ## Convert the registry value type to one that reg.exe can understand
                    switch ($instance.Type)
                    {
                        'String' {
                            $RegValueType = 'REG_SZ'
                        }
                        'Dword' {
                            $RegValueType = 'REG_DWORD'
                        }
                        'Binary' {
                            $RegValueType = 'REG_BINARY'
                        }
                        'ExpandString' {
                            $RegValueType = 'REG_EXPAND_SZ'
                        }
                        'MultiString' {
                            $RegValueType = 'REG_MULTI_SZ'
                        }
                        default
                        {
                            throw "Registry type '$($instance.Type)' not recognized"
                        }
                    }
                    if (-not (Get-Item -Path "HKCU:\$($instance.Path)" -ErrorAction 'SilentlyContinue'))
                    {
                        if ($PSCmdlet.ShouldProcess($instance.Path, 'New'))
                        {
                            Write-Log -Message "The registry key 'HKCU:\$($instance.Path)'' does not exist. Creating..."
                            New-Item -Path "HKCU:\$($instance.Path) | Split-Path -Parent)" -Name ("HKCU:\$($instance.Path)" | Split-Path -Leaf) -Force | Out-Null
                        }
                    }
                    $CommandLine = "reg add `"{0}`" /v {1} /t {2} /d {3} /f" -f "HKCU\$($instance.Path)", $instance.Name, $RegValueType, $instance.Value
                }
                if ($PSCmdlet.ShouldProcess($CommandLine,'set all user action')) {
                    Set-AllUserStartupAction -CommandLine $CommandLine
                }    
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-MsiexecInstallString
{
    [OutputType([string])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$InstallerFilePath,
    
        [Parameter()]
        [AllowNull()]
        [string[]]$MstFilePath,
    
        [Parameter()]
        [AllowNull()]
        [string]$MspFilePath,
    
        [Parameter()]
        [AllowNull()]
        [string]$ExtraSwitches,
    
        [Parameter()]
        [AllowNull()]
        [string]$LogFilePath
    )
    begin {
        $ErrorActionPreference = 'Stop'
    }
    process {
        try
        {
            ## We're creating common msiexec switches here. /i specifies I want to run an install, /qn
            ## says I want that install to be quiet (no prompts) and n means no UI so no progress bars
            $InstallArgs = @()
            $InstallArgs += "/i `"$InstallerFilePath`" /qn"
            if ($MstFilePath)
            {
                $MstFilePath = $MstFilePath -join ';'
                $InstallArgs += "TRANSFORMS=`"$MstFilePath`""
            }
            if ($MspFilePath)
            {
                $InstallArgs += "PATCH=`"$MspFilePath`""
            }
            if ($ExtraSwitches)
            {
                $InstallArgs += $ExtraSwitches
            }
            
            ## Once we've added all of the custom syntax elements we'll then add a few more default
            ## switches. REBOOT=ReallySuppress prevents the computer from rebooting if it exists with an
            ## exit code of 3010, ALLUSERS=1 means that we'd like to make this software for all users
            ## on the machine and /Lvx* is the most verbose way to specify a log file path and to log as
            ## much information as possible.
            if (-not $LogFilePath)
            {
                $LogFilePath = "$(Get-SystemTempFolderPath)\$($InstallerFilePath | Split-Path -Leaf).log"
            }
            $InstallArgs += "REBOOT=ReallySuppress ALLUSERS=1 /Lvx* `"$LogFilePath`""
            $InstallArgs = $InstallArgs -join ' '
            $InstallArgs
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Uninstall-ViaMsizap
{
    <#
    .SYNOPSIS
        This function runs the MSIzap utility to forcefully remove and cleanup MSI-installed software
    .DESCRIPTION
        This function runs msizap to remove software.
    .EXAMPLE
        Uninstall-ViaMsizap -MsizapFilePath C:\msizap.exe -Guid {XXXX-XXX-XXX}
        This example would attempt to remove the software registered with the GUID {XXXX-XXX-XXX}.
    .PARAMETER MsizapFilePath
        The file path where the msizap utility exists. This can be a local or UNC path.
    .PARAMETER Guid
        The GUID of the registered software you'd like removed
    .PARAMETER Params
        Non-default params you'd like passed to msizap. By default, "TWG!" is used to remove in all user
        profiles. This typically doesn't need to be changed.
    .PARAMETER LogFilePath
        The file path to where msizap will generate output
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
        [Parameter(Mandatory = $true)]
        [string]$Guid,
        
        [string]$Params = 'TWG!',
        
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string]$MsizapFilePath = "C:\MyDeployment\msizap.exe",
        
        [Parameter()]
        [string]$LogFilePath = "$(Get-SystemTempFolderPath)\msizap.log"
    )
    process
    {
        try
        {
            Write-Log -Message "-Starting the process `"$MsiZapFilePath $Params $Guid`"..."
            $NewProcess = Start-Process $MsiZapFilePath -ArgumentList "$Params $Guid" -Wait -NoNewWindow -PassThru -RedirectStandardOutput $LogFilePath
            Wait-MyProcess -ProcessId $NewProcess.Id    
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Uninstall-WindowsInstallerPackage
{
    <#
    .SYNOPSIS
        This function runs an uninstall for a Windows Installer package
    .PARAMETER Name
        The software title of the Windows installer package you'd like to uninstall. Use either the Name
        param or the Guid param to find the Windows installer package.
    .PARAMETER MsiExecSwitches
        Specify a string of switches you'd like msiexec.exe to run when it attempts to uninstall the software. By default,
        it already uses "/x GUID /qn". You can specify any additional parameters here.
    .PARAMETER Guid
        The GUID of the Windows Installer package
    #>

    [OutputType([void])]
    [CmdletBinding(DefaultParameterSetName = 'Guid')]
    param (
        [Parameter(ParameterSetName = 'Name')]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'Guid')]
        [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
        [string]$Guid,
        
        [string]$MsiExecSwitches
    )
    process
    {
        try
        {
            $Params = @{ }
            if ($PSBoundParameters.ContainsKey('Name'))
            {
                Write-Log -Message "Attempting to uninstall Windows Installer using name '$Name'..."
                $params.Name = $Name
            }
            if ($PSBoundParameters.ContainsKey('Guid'))
            {
                Write-Log -Message "Attempting to uninstall Windows Installer using GUID '$Guid'..."
                $params.Guid = $Guid
            }
            if ($PSBoundParameters.ContainsKey('MsiExecSwitches'))
            {
                $params.MsiExecSwitches = $MsiExecSwitches
            }
            
            Uninstall-WindowsInstallerPackageWithMsiexec @params
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Uninstall-WindowsInstallerPackageWithMsiexec
{
    <#
    .SYNOPSIS
        This function runs an uninstall for a Windows Installer package using msiexec.exe /x
    .PARAMETER Name
        The software title of the Windows installer package you'd like to uninstall. Use either the Name
        param or the Guid param to find the Windows installer package.
    .PARAMETER Guid
        The GUID of the Windows Installer package
    .PARAMETER MsiExecSwitches
        Specify a string of switches you'd like msiexec.exe to run when it attempts to uninstall the software. By default,
        it already uses "/x GUID /qn". You can specify any additional parameters here.
    #>

    [OutputType([void])]
    [CmdletBinding(DefaultParameterSetName = 'Guid')]
    param (
        [Parameter(ParameterSetName = 'Name')]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'Guid')]
        [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
        [string]$Guid,
        
        [string]$MsiExecSwitches
    )
    process
    {
        try {
            if ($PSBoundParameters.ContainsKey('Name'))
            {
                Write-Log -Message "Attempting to uninstall Windows Installer with msiexec.exe using name '$Name'..."
                $Params = @{ 'Name' = $Name }
                $software = Get-InstalledSoftware @Params
                if (-not $software)
                {
                    throw 'Name specified for uninstall but could not find GUID to remove'
                }
                else
                {
                    ## Sometimes multiple instances are returned. 1 having no GUID and 1 having a GUID.
                    ## Cisco AnyConnect is an example where if the one with the GUID is removed both are removed.
                    $Guid = $software | Where-Object { $_.GUID }
                    if (-not $Guid)
                    {
                        throw 'Required GUID could not be found for software'
                    }
                    else
                    {
                        $Guid = $Guid.GUID
                        Write-Log -Message "Using GUID [$Guid] for the uninstall"
                    }
                }
            }
            
            $switches = @("/x `"$Guid`"")
            if ($PSBoundParameters.ContainsKey('MsiExecSwitches'))
            {
                $switches += $MsiExecSwitches
            }
            $switches += @('REBOOT=ReallySuppress', '/qn')
            $switchString = $switches -join ' '
            
            Write-Log -Message "Initiating msiexec.exe with arguments [$($switchString)]"
            $Process = Start-Process 'msiexec.exe' -ArgumentList $switchString -PassThru -Wait -NoNewWindow
            Wait-WindowsInstaller
            if (-not (Test-InstalledSoftware -Guid $Guid))
            {
                Write-Log -Message 'Successfully uninstalled MSI package with msiexec.exe'
            }
            else
            {
                throw 'Failed to uninstall MSI package with msiexec.exe'
            }
        }
        catch 
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
        
    }
}

function Uninstall-WindowsInstallerPackageWithMsiModule
{
    <#
    .SYNOPSIS
        This function runs an uninstall for a Windows Installer package using the Windows Installer Powershell module
        https://psmsi.codeplex.com
    .PARAMETER Name
        The software title of the Windows installer package you'd like to uninstall. Use either the Name
        param or the Guid param to find the Windows installer package.
    .PARAMETER Guid
        The GUID of the Windows Installer package
    #>

    [OutputType([void])]
    [CmdletBinding(DefaultParameterSetName = 'Guid')]
    param (
        [Parameter(ParameterSetName = 'Name')]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'Guid')]
        [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
        [string]$Guid
    )
    process
    {
        try
        {    
            if (-not (Get-Module -ListAvailable -Name 'MSI'))
            {
                throw 'Required MSI module is not available'
            }
            
            if (((Get-OperatingSystem) -notmatch 'XP') -and ((Get-OperatingSystem) -notmatch 'Server'))
            {
                $UninstallParams = @{
                    'Log' = $script:LogFilePath
                    'Chain' = $true
                    'Force' = $true
                    'ErrorAction' = 'SilentlyContinue'
                    'Properties' = 'REBOOT=ReallySuppress'
                }
                
                if ($Name)
                {
                    $MsiProductParams = @{ 'Name' = $Name }
                }
                elseif ($Guid)
                {
                    $MsiProductParams = @{ 'ProductCode' = $Guid }
                }
                
                Get-MSIProductInfo @MsiProductParams | Uninstall-MsiProduct @UninstallParams
                if (-not (Test-InstalledSoftware @MsiProductParams))
                {
                    Write-Log -Message "Successfully uninstalled MSI package '$Name' with MSI module"
                }
                else
                {
                    throw "Failed to uninstall MSI package '$Name' with MSI module"
                }
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Wait-WindowsInstaller
{
    <#
    .SYNOPSIS
        This function should be called immediately after the Uninstall-WindowsInstallerPackage function. This is a specific
        process waiting function especially for msiexec.exe. It was built because the Wait-MyProcess function will sometimes
        not work with msiexec installs/uninstalls. This is because msiexec.exe creates a process tree which does not necessarily
        mean child processes. Using this function will ensure your script always wait for the msiexec.exe process you
        kicked off to complete before continuing.
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            
            Write-Log -Message 'Looking for any msiexec.exe processes...'
            $MsiexecProcesses = Get-WmiObject -Class Win32_Process -Filter "Name = 'msiexec.exe'" | Where-Object { $_.CommandLine -ne 'C:\Windows\system32\msiexec.exe /V' }
            if ($MsiExecProcesses)
            {
                Write-Log -Message "Found '$(@($MsiexecProcesses).Count)' Windows installer processes. Waiting..."
                ## Wait for each msiexec.exe process to finish before proceeding
                foreach ($Process in $MsiexecProcesses)
                {
                    Wait-MyProcess -ProcessId $Process.ProcessId
                }
                ## When all msiexec.exe processes finish, recursively call this function again to ensure no
                ## other installs have begun.
                Wait-WindowsInstaller
            }
            else
            {
                Write-Log -Message 'No Windows installer processes found'
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-AllUsersDesktopFolderPath
{
    <#
    .SYNOPSIS
        Because sometimes the all users desktop folder path can be different this function is a placeholder to find
        the all users desktop folder path. It uses a shell object to find this path.
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            $Shell = New-Object -ComObject "WScript.Shell"
            $Shell.SpecialFolders.Item('AllUsersDesktop')
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-AllUsersProfileFolderPath
{
    <#
    .SYNOPSIS
        Because sometimes the all users profile folder path can be different this function is a placeholder to find
        the all users profile folder path ie. C:\ProgramData or C:\Users\All Users. It uses an environment variable
        to find this path.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            $env:ALLUSERSPROFILE    
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-AllUsersStartMenuFolderPath
{
    <#
    .SYNOPSIS
        Because sometimes the all users profile folder path can be different this function is a placeholder to find
        the start menu in the all users profile folder path ie. C:\ProgramData or C:\Users\All Users.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            
            if (((Get-OperatingSystem) -match 'XP') -or ((Get-OperatingSystem) -match '2003'))
            {
                "$(Get-AllUsersProfileFolderPath)\Start Menu"
            }
            else
            {
                "$(Get-AllUsersProfileFolderPath)\Microsoft\Windows\Start Menu"
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-UserProfile
{
    <#
    .SYNOPSIS
        This function queries the registry to find all of the user profiles
    #>

    [OutputType([System.Management.Automation.PSCustomObject])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$ExcludeSystemProfiles
    )
    process
    {
        try
        {
            if ($ExcludeSystemProfiles.IsPresent) {
                $whereFilter = { $_.SID.Length -ge 45 }
            } else {
                $whereFilter = { '*' }
            }
            
            $profiles = Get-ItemProperty 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\*'
            
            $selectProps = @(
                '*', 
                @{ n = 'SID'; e = { $_.PSChildName }}, @{ n = 'Username'; e = { $_.ProfileImagePath | Split-Path -Leaf }}
            )
            $profiles | Select-Object -ExcludeProperty SID -Property $selectProps | Where -FilterScript $whereFilter
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-RootUserProfileFolderPath
{
    <#
    .SYNOPSIS
        Because sometimes the root user profile folder path can be different this function is a placeholder to find
        the root user profile folder path ie. C:\Users or C:\Documents and Settings for any OS. It queries a registry value
        to find this path.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory).ProfilesDirectory    
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-UserProfilePath
{
    <#
    .SYNOPSIS
        This function find the folder path of a user profile based off of a number of different criteria. If no criteria is
        used, it will return all user profile paths.
    .EXAMPLE
        PS> .\Get-UserProfilePath -Sid 'S-1-5-21-350904792-1544561288-1862953342-32237'
     
        This example finds the user profile path based on the user's SID
    .EXAMPLE
        PS> .\Get-UserProfilePath -Username 'bob'
     
        This example finds the user profile path based on the username
    .PARAMETER Sid
         The user SID
    .PARAMETER Username
        The username
    #>

    [OutputType([string])]
    [CmdletBinding(DefaultParameterSetName = 'None')]
    param (
        [Parameter(ParameterSetName = 'SID')]
        [string]$Sid,
        
        [Parameter(ParameterSetName = 'Username')]
        [string]$Username
    )
    
    process
    {
        try
        {
            if ($Sid)
            {
                $WhereBlock = { $_.PSChildName -eq $Sid }
            }
            elseif ($Username)
            {
                $WhereBlock = { $_.GetValue('ProfileImagePath').Split('\')[-1] -eq $Username }
            }
            else
            {
                $WhereBlock = { $null -ne $_.PSChildName }
            }
            Get-ChildItem 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList' | Where-Object $WhereBlock | ForEach-Object { $_.GetValue('ProfileImagePath') }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Remove-ProfileItem
{
    <#
    .SYNOPSIS
        This function removes a file(s) or folder(s) with the same path in all user profiles including
        system profiles like SYSTEM, NetworkService and AllUsers.
    .EXAMPLE
        PS> .\Remove-ProfileItem -Path 'AppData\Adobe'
     
        This example will remove the folder path 'AppData\Adobe' from all user profiles
    .PARAMETER Path
        The path(s) to the file or folder you'd like to remove.
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$Path
    )
    
    process
    {
        try
        {
            
            $AllUserProfileFolderPath = Get-AllUsersProfileFolderPath
            $UserProfileFolderPaths = Get-UserProfilePath
            
            foreach ($p in $Path)
            {
                if (-not (Test-Path "$AllUserProfileFolderPath\$p"))
                {
                    Write-Log -Message "The folder '$AllUserProfileFolderPath\$p' does not exist"
                }
                else
                {
                    Remove-Item -Path "$AllUserProfileFolderPath\$p" -Force -Recurse
                }
                
                
                foreach ($ProfilePath in $UserProfileFolderPaths)
                {
                    if (-not (Test-Path "$ProfilePath\$p"))
                    {
                        Write-Log -Message "The folder '$ProfilePath\$p' does not exist"
                    }
                    else
                    {
                        Remove-Item -Path "$ProfilePath\$p" -Force -Recurse
                    }
                }
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Set-AllUserStartupAction
{
    <#
    .SYNOPSIS
        A function that executes a command line for the any current logged on user and uses the Active Setup registry key to set a
        registry value that contains a command line EXE with arguments that will be executed once for every user that logs in.
    .PARAMETER CommandLine
        The command line string that will be executed once at every user logon
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [string]$CommandLine
    )
    process
    {
        try
        {
            if ($PSCmdlet.ShouldProcess($CommandLine,'set all startup action')) {
                ## Create the Active Setup registry key so that the reg add cmd will get ran for each user
                ## logging into the machine.
                ## http://www.itninja.com/blog/view/an-active-setup-primer
                $Guid = [guid]::NewGuid().Guid
                Write-Log -Message "Created GUID '$Guid' to use for Active Setup"
                $ActiveSetupRegParentPath = 'HKLM:\Software\Microsoft\Active Setup\Installed Components'
                New-Item -Path $ActiveSetupRegParentPath -Name $Guid -Force | Out-Null
                $ActiveSetupRegPath = "HKLM:\Software\Microsoft\Active Setup\Installed Components\$Guid"
                Write-Log -Message "Using registry path '$ActiveSetupRegPath'"
                Write-Log -Message "Setting command line registry value to '$CommandLine'"
                Set-ItemProperty -Path $ActiveSetupRegPath -Name '(Default)' -Value 'Active Setup Test' -Force
                Set-ItemProperty -Path $ActiveSetupRegPath -Name 'Version' -Value '1' -Force
                Set-ItemProperty -Path $ActiveSetupRegPath -Name 'StubPath' -Value $CommandLine -Force
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Compare-FilePath
{
    <#
    .SYNOPSIS
        This function checks the hash of 2 files see if they are the same
    .EXAMPLE
        PS> Compare-FilePath -ReferencePath 'C:\Windows\file.txt' -DifferencePath '\\COMPUTER\c$\Windows\file.txt'
     
        This example checks to see if the file C:\Windows\file.txt is exactly the same as the file \\COMPUTER\c$\Windows\file.txt
    .PARAMETER ReferencePath
        The first file path to compare
    .PARAMETER DifferencePath
        The second file path to compare
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$ReferenceFilePath,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$DifferenceFilePath
    )
    process
    {
        try
        {
            $ReferenceHash = Get-MyFileHash -Path $ReferenceFilePath
            $DifferenceHash = Get-MyFileHash -Path $DifferenceFilePath
            $ReferenceHash.SHA256 -ne $DifferenceHash.SHA256
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Compare-FolderPath
{
    <#
    .SYNOPSIS
        This function checks all files inside of a folder against another folder to see if they are the same
    .EXAMPLE
        PS> Compare-FilePath -ReferencePath 'C:\Windows' -DifferencePath '\\COMPUTER\c$\Windows'
     
        This example checks to see if the contents in C:\Windows is exactly the same as the contents in \\COMPUTER\c$\Windows
    .PARAMETER ReferencePath
        The first folder path to compare
    .PARAMETER DifferencePath
        The second folder path to compare
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [string]$ReferenceFolderPath,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [string]$DifferenceFolderPath
    )
    process
    {
        try
        {
            $ReferenceFiles = Get-ChildItem -Path $ReferenceFolderPath -Recurse | Where-Object { -not $_.PsIsContainer }
            $DifferenceFiles = Get-ChildItem -Path $DifferenceFolderPath -Recurse | Where-Object { -not $_.PsIsContainer }
            if ($ReferenceFiles.Count -ne $DifferenceFiles.Count)
            {
                Write-Log -Message "Folder path '$ReferenceFolderPath' and '$DifferenceFolderPath' file counts are different" -LogLevel '2'
                $false
            }
            elseif (Compare-Object -ReferenceObject ($ReferenceFiles | Get-MyFileHash) -DifferenceObject ($DifferenceFiles | Get-MyFileHash))
            {
                Write-Log -Message "Folder path '$ReferenceFolderPath' and '$DifferenceFolderPath' file hashes are different" -LogLevel '2'
                $false
            }
            else
            {
                Write-Log -Message "Folder path '$ReferenceFolderPath' and '$DifferenceFolderPath' have equal contents"
                $true
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Copy-FileWithHashCheck
{
    <#
    .SYNOPSIS
        This function copies a file and then verifies the copy was successful by comparing the source and destination
        file hash values.
    .EXAMPLE
        PS> Copy-FileWithHashCheck -SourceFilePath 'C:\Windows\file1.txt' -DestinationFolderPath '\\COMPUTER\c$\Windows\file2.txt'
     
        This example copied the file from C:\Windows\file1.txt to \\COMPUTER\c$\Windows and then checks the hash for the
        source file and destination file to ensure the copy was successful.
    .PARAMETER SourceFilePath
        The source file path
    .PARAMETER DestinationFolderPath
        The destination folder path
    .PARAMETER Force
        Overwrite the destination file if one exists
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $True)]
        [Alias('Fullname')]
        [string]$SourceFilePath,
        
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [string]$DestinationFolderPath,
        
        [Parameter()]
        [switch]$Force
    )
    process
    {
        try
        {
            $CopyParams = @{ 'Path' = $SourceFilePath; 'Destination' = $DestinationFolderPath }
            
            ## If the file is already there, check to see if it's the one we're copying in the first place
            $DestFilePath = "$DestinationFolderPath\$($SourceFilePath | Split-Path -Leaf)"
            if (Test-Path -Path $DestFilePath -PathType 'Leaf')
            {
                if (Compare-FilePath -ReferenceFilePath $SourceFilePath -DifferenceFilePath $DestFilePath)
                {
                    Write-Log -Message "The file $SourceFilePath is already in $DestinationFolderPath and is the same. No need to copy"
                }
                elseif (-not $Force.IsPresent)
                {
                    throw "The file $SourceFilePath is already in $DestinationFolderPath but is not the same file being copied and -Force was not used."
                }
                else
                {
                    $CopyParams.Force = $true
                }
            }
            Write-Log -Message "Copying [$($CopyParams.Path)] to [[$($CopyParams.Destination)]...."
            Copy-Item @CopyParams
            if (Compare-FilePath -ReferenceFilePath $SourceFilePath -DifferenceFilePath $DestFilePath)
            {
                Write-Log -Message "The file $SourceFilePath was successfully copied to $DestinationFolderPath."
            }
            throw "Attempted to copy the file $SourceFilePath to $DestinationFolderPath but failed the hash check"
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Find-InTextFile
{
    <#
    .SYNOPSIS
        Performs a find (or replace) on a string in a text file or files.
    .EXAMPLE
        PS> Find-InTextFile -FilePath 'C:\MyFile.txt' -Find 'water' -Replace 'wine'
     
        Replaces all instances of the string 'water' into the string 'wine' in
        'C:\MyFile.txt'.
    .EXAMPLE
        PS> Find-InTextFile -FilePath 'C:\MyFile.txt' -Find 'water'
     
        Finds all instances of the string 'water' in the file 'C:\MyFile.txt'.
    .PARAMETER FilePath
        The file path of the text file you'd like to perform a find/replace on.
    .PARAMETER Find
        The string you'd like to replace.
    .PARAMETER Replace
        The string you'd like to replace your 'Find' string with.
    .PARAMETER UseRegex
        Use this switch parameter if you're finding strings using regex else the Find string will
        be escaped from regex characters
    .PARAMETER NewFilePath
        If a new file with the replaced the string needs to be created instead of replacing
        the contents of the existing file use this param to create a new file.
    .PARAMETER Force
        If the NewFilePath param is used using this param will overwrite any file that
        exists in NewFilePath.
    #>

    [OutputType([Microsoft.PowerShell.Commands.MatchInfo])]
    [CmdletBinding(DefaultParameterSetName = 'NewFile')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [string[]]$FilePath,
        
        [Parameter(Mandatory = $true)]
        [string]$Find,
        
        [Parameter()]
        [string]$Replace,
        
        [Parameter()]
        [switch]$UseRegex,
        
        [Parameter(ParameterSetName = 'NewFile')]
        [ValidateScript({ Test-Path -Path ($_ | Split-Path -Parent) -PathType 'Container' })]
        [string]$NewFilePath,
        
        [Parameter(ParameterSetName = 'NewFile')]
        [switch]$Force
    )
    begin
    {
        if (-not $UseRegex.IsPresent)
        {
            $Find = [regex]::Escape($Find)
        }
    }
    process
    {
        try
        {
            foreach ($File in $FilePath)
            {
                if ($Replace)
                {
                    if ($NewFilePath)
                    {
                        if ((Test-Path -Path $NewFilePath -PathType 'Leaf') -and $Force.IsPresent)
                        {
                            Remove-Item -Path $NewFilePath -Force
                            (Get-Content $File) -replace $Find, $Replace | Add-Content -Path $NewFilePath -Force
                        }
                        elseif ((Test-Path -Path $NewFilePath -PathType 'Leaf') -and (-not $Force.IsPresent))
                        {
                            Write-Log -Message "The file at '$NewFilePath' already exists and the -Force param was not used" -LogLevel 2
                        }
                        else
                        {
                            (Get-Content $File) -replace $Find, $Replace | Add-Content -Path $NewFilePath -Force
                        }
                    }
                    else
                    {
                        (Get-Content $File) -replace $Find, $Replace | Add-Content -Path "$File.tmp" -Force
                        Remove-Item -Path $File
                        Rename-Item -Path "$File.tmp" -NewName $File
                    }
                }
                else
                {
                    Select-String -Path $File -Pattern $Find
                }
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Register-File
{
    <#
    .SYNOPSIS
        A function that uses the utility regsvr32.exe utility to register a file
    .PARAMETER FilePath
        The file path
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [string]$FilePath
    )
    process
    {
        try
        {
            
            $Result = Start-Process -FilePath 'regsvr32.exe' -ArgumentList "/s `"$FilePath`"" -Wait -NoNewWindow -PassThru
            Wait-MyProcess -ProcessId $Result.Id
            if ($Result.ExitCode -ne '0')
            {
                throw "Process ID [$($Result.Id)] failed. Exit code was [$($Result.ExitCode)]"
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Set-MyFileSystemAcl
{
    <#
    .SYNOPSIS
        This function allows an easy method to set a file system access ACE
    .PARAMETER Path
         The file path of a file
    .PARAMETER Identity
        The security principal you'd like to set the ACE to. This should be specified like
        DOMAIN\user or LOCALMACHINE\User.
    .PARAMETER Right
        One of many file system rights. For a list http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights(v=vs.110).aspx
    .PARAMETER InheritanceFlags
        The flags to set on how you'd like the object inheritance to be set. Possible values are
        ContainerInherit, None or ObjectInherit. http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags(v=vs.110).aspx
    .PARAMETER PropagationFlags
        The flag that specifies on how you'd permission propagation to behave. Possible values are
        InheritOnly, None or NoPropagateInherit. http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags(v=vs.110).aspx
    .PARAMETER Type
        The type (Allow or Deny) of permissions to add. http://msdn.microsoft.com/en-us/library/w4ds5h86(v=vs.110).aspx
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ })]
        [string]$Path,
        
        [Parameter(Mandatory = $true)]
        [string]$Identity,
        
        [Parameter(Mandatory = $true)]
        [string]$Right,
        
        [Parameter(Mandatory = $true)]
        [string]$InheritanceFlags,
        
        [Parameter(Mandatory = $true)]
        [string]$PropagationFlags,
        
        [Parameter(Mandatory = $true)]
        [string]$Type
    )
    
    process
    {
        try
        {
            $Acl = (Get-Item $Path).GetAccessControl('Access')
            $Ar = New-Object System.Security.AccessControl.FileSystemAccessRule($Identity, $Right, $InheritanceFlags, $PropagationFlags, $Type)
            $Acl.SetAccessRule($Ar)
            if ($PSCmdlet.ShouldProcess($Path, 'ACL Change'))
            {
                Set-Acl $Path $Acl
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-FileVersion
{
    <#
    .SYNOPSIS
        This function finds the file version of a file. This is useful for applications that don't
        register themselves properly with Windows Installer
    .PARAMETER FilePath
         A valid file path
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
        [string]$FilePath
    )
    process
    {
        try
        {
            (Get-ItemProperty -Path $FilePath).VersionInfo.FileVersion    
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-MyFileHash
{
    <#
        .SYNOPSIS
            Calculates the hash on a given file based on the seleced hash algorithm.
 
        .DESCRIPTION
            Calculates the hash on a given file based on the seleced hash algorithm. Multiple hashing
            algorithms can be used with this command.
 
        .PARAMETER Path
            File or files that will be scanned for hashes.
 
        .PARAMETER Algorithm
            The type of algorithm that will be used to determine the hash of a file or files.
            Default hash algorithm used is SHA256. More then 1 algorithm type can be used.
             
            Available hash algorithms:
 
            MD5
            SHA1
            SHA256 (Default)
            SHA384
            SHA512
            RIPEM160
 
        .NOTES
            Name: Get-FileHash
            Author: Boe Prox
            Created: 18 March 2013
            Modified: 28 Jan 2014
                1.1 - Fixed bug with incorrect hash when using multiple algorithms
 
        .OUTPUTS
            System.IO.FileInfo.Hash
 
        .EXAMPLE
            Get-FileHash -Path Test2.txt
            Path SHA256
            ---- ------
            C:\users\prox\desktop\TEST2.txt 5f8c58306e46b23ef45889494e991d6fc9244e5d78bc093f1712b0ce671acc15
             
            Description
            -----------
            Displays the SHA256 hash for the text file.
 
        .EXAMPLE
            Get-FileHash -Path .\TEST2.txt -Algorithm MD5,SHA256,RIPEMD160 | Format-List
            Path : C:\users\prox\desktop\TEST2.txt
            MD5 : cb8e60205f5e8cae268af2b47a8e5a13
            SHA256 : 5f8c58306e46b23ef45889494e991d6fc9244e5d78bc093f1712b0ce671acc15
            RIPEMD160 : e64d1fa7b058e607319133b2aa4f69352a3fcbc3
 
            Description
            -----------
            Displays MD5,SHA256 and RIPEMD160 hashes for the text file.
 
        .EXAMPLE
            Get-ChildItem -Filter *.exe | Get-FileHash -Algorithm MD5
            Path MD5
            ---- ---
            C:\users\prox\desktop\handle.exe 50c128c5b28237b3a01afbdf0e546245
            C:\users\prox\desktop\PortQry.exe c6ac67f4076ca431acc575912c194245
            C:\users\prox\desktop\procexp.exe b4caa7f3d726120e1b835d52fe358d3f
            C:\users\prox\desktop\Procmon.exe 9c85f494132cc6027762d8ddf1dd5a12
            C:\users\prox\desktop\PsExec.exe aeee996fd3484f28e5cd85fe26b6bdcd
            C:\users\prox\desktop\pskill.exe b5891462c9ca5bddfe63d3bae3c14e0b
            C:\users\prox\desktop\Tcpview.exe 485bc6763729511dcfd52ccb008f5c59
 
            Description
            -----------
            Uses pipeline input from Get-ChildItem to get MD5 hashes of executables.
 
    #>

    [OutputType([PSObject])]
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $True)]
        [Alias("PSPath", "FullName")]
        [string[]]$Path,
        
        [Parameter(Position = 1)]
        [ValidateSet("MD5", "SHA1", "SHA256", "SHA384", "SHA512", "RIPEMD160")]
        [string[]]$Algorithm = "SHA256"
    )
    Process
    {
        
        ForEach ($item in $Path)
        {
            try
            {
                $item = (Resolve-Path $item).ProviderPath
                If (-Not ([uri]$item).IsAbsoluteUri)
                {
                    Write-Log -Message ("{0} is not a full path, using current directory: {1}" -f $item, $pwd)
                    $item = (Join-Path $pwd ($item -replace "\.\\", ""))
                }
                If (Test-Path $item -PathType Container)
                {
                    Write-Log -Message ("Cannot calculate hash for directory: {0}" -f $item) -LogLevel 2
                    Return
                }
                $object = New-Object PSObject -Property @{
                    Path = $item
                }
                #Open the Stream
                $stream = ([IO.StreamReader]$item).BaseStream
                foreach ($Type in $Algorithm)
                {
                    [string]$hash = -join ([Security.Cryptography.HashAlgorithm]::Create($Type).ComputeHash($stream) |
                    ForEach-Object { "{0:x2}" -f $_ })
                    $null = $stream.Seek(0, 0)
                    #If multiple algorithms are used, then they will be added to existing object
                    $object = Add-Member -InputObject $Object -MemberType NoteProperty -Name $Type -Value $Hash -PassThru
                }
                $object.pstypenames.insert(0, 'System.IO.FileInfo.Hash')
                #Output an object with the hash, algorithm and path
                Write-Output $object
                
                #Close the stream
                $stream.Close()
                
            }
            catch
            {
                Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }
    }
}

function Get-RootUserProfileFolderPath
{
    <#
    .SYNOPSIS
        Because sometimes the root user profile folder path can be different this function is a placeholder to find
        the root user profile folder path ie. C:\Users or C:\Documents and Settings for any OS. It queries a registry value
        to find this path.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param ()
    process
    {
        try
        {
            (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Name ProfilesDirectory).ProfilesDirectory    
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-Shortcut
{
    <#
    .SYNOPSIS
        This function searches for files matching a LNK and URL extension.
    .DESCRIPTION
        This function, by default, recursively searches for files matching a LNK and URL extensions containing
        a specific string inside the target path, name or both. If no folder path specified, it will
        recursively search all user profiles and the all users profile.
    .NOTES
        Created on: 6/23/2014
        Created by: Adam Bertram
    .EXAMPLE
        Get-Shortcut -MatchingTargetPath 'http:\\servername\local'
        This example would find all shortcuts (URL and LNK) in all user profiles that have a
        target path that match 'http:\\servername\local'
    .EXAMPLE
        Get-Shortcut -MatchingTargetPath 'http:\\servername\local' -MatchingName 'name'
        This example would find all shortcuts (URL and LNK) in all user profiles that have a
        target path that match 'http:\\servername\local' and have a name containing the string "name"
    .EXAMPLE
        Get-Shortcut -MatchingTargetPath 'http:\\servername\local' -MatchingFilePath 'C:\Users\abertram\Desktop'
        This example would find all shortcuts (URL and LNK) in the 'C:\Users\abertram\Desktop file path
        that have a target path that match 'http:\\servername\local' and have a name containing the
        string "name"
    .PARAMETER MatchingTargetPath
        The string you'd like to search for inside the shortcut's target path
    .PARAMETER MatchingName
        A string you'd like to search for inside of the shortcut's name
    .PARAMETER MatchingFilePath
        A string you'd like to search for inside of the shortcut's file path
    .PARAMETER FolderPath
        The folder path to search for shortcuts in. You can specify multiple folder paths. This defaults to
        the user profile root and the all users profile
    .PARAMETER NoRecurse
        This turns off recursion on the folder path specified searching subfolders of the FolderPath
    #>

    [OutputType([System.IO.FileInfo])]
    [CmdletBinding()]
    param (
        [string]$MatchingTargetPath,
        
        [string]$MatchingName,
        
        [string]$MatchingFilePath,
        
        [string[]]$FolderPath = ((Get-RootUserProfileFolderPath), $env:ALLUSERSPROFILE),
        
        [switch]$NoRecurse
    )
    process
    {
        try
        {    
            $Params = @{
                Include = @('*.url', '*.lnk')
                ErrorAction = 'SilentlyContinue'
                Force = $true
            }
            
            if (-not $NoRecurse)
            {
                $Params['Recurse'] = $true
            }
            
            $ShellObject = New-Object -ComObject Wscript.Shell
            
            foreach ($Path in $FolderPath)
            {
                try
                {
                    Write-Verbose -Message "Searching for shortcuts in $Path..."
                    $WhereConditions = @()
                    $Params['Path'] = $Path
                    if ($MatchingTargetPath)
                    {
                        $WhereConditions += '(($ShellObject.CreateShortcut($_.FullName)).TargetPath -like "*$MatchingTargetPath*")'
                    }
                    if ($MatchingName)
                    {
                        $WhereConditions += '($_.Name -like "*$MatchingName*")'
                    }
                    if ($MatchingFilePath)
                    {
                        $WhereConditions += '($_.FullName -like "*$MatchingFilePath*")'
                    }
                    if (@($WhereConditions).Count -gt 0)
                    {
                        $WhereBlock = [scriptblock]::Create($WhereConditions -join ' -and ')
                        ## TODO: Figure out a way to make this cleanly log access denied errors and continue
                        Get-ChildItem @Params | Where-Object $WhereBlock
                    }
                    else
                    {
                        Get-ChildItem @Params
                    }
                    Write-Verbose -Message "Finished searching for shortcuts in $Path..."
                }
                catch
                {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
            
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function New-Shortcut
{
    <#
    .SYNOPSIS
        This function creates a file shortcut
    .NOTES
        Created on: 07/19/2014
        Created by: Adam Bertram
    .EXAMPLE
        New-Shortcut -FolderPath 'C:\' -Name 'My Shortcut' -TargetFilePath 'C:\Windows\notepad.exe'
        This examples creates a shortcut in C:\ called 'My Shortcut.lnk' pointing to notepad.exe
    .EXAMPLE
        New-Shortcut -CommonLocation AllUsersDesktop -Name 'My Shortcut' -TargetFilePath 'C:\Windows\notepad.exe'
        This examples creates a shortcut on the all users desktop called 'My Shortcut.lnk' pointing to notepad.exe
    .PARAMETER FolderPath
        If a custom path is needed that's not included in the list of common locations in the CommonLocation parameter
        this parameter can be used to create a folder in the specified path.
    .PARAMETER CommonLocation
        This is a set of common locations shortcuts are typically created in. Use this parameter if you'd like to
        quickly specify where the shortcut needs to be created in.
    .PARAMETER Name
        The name of the shortcut (file)
    .PARAMETER TargetPath
        The file path or URL of the application you'd like the shortcut to point to
    .PARAMETER Arguments
        File arguments you'd like to append to the target file path
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName = 'CommonLocation')]
    param (
        [Parameter(ParameterSetName = 'CustomLocation',Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType 'Container' })]
        [string]$FolderPath,
        
        [Parameter(ParameterSetName = 'CommonLocation',Mandatory)]
        [ValidateSet('AllUsersDesktop')]
        [string]$CommonLocation,
        
        [Parameter(Mandatory)]
        [string]$Name,
        
        [Parameter(Mandatory)]
        [string]$TargetPath,
        
        [Parameter()]
        [string]$Arguments
    )
    begin
    {
        try
        {
            $ShellObject = New-Object -ComObject Wscript.Shell
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    process
    {
        try
        {
            
            if ($TargetPath -notmatch '^\w{1}:\\')
            {
                $Extension = 'url'
            }
            else
            {
                $Extension = 'lnk'
            }
            if ($CommonLocation -eq 'AllUsersDesktop')
            {
                $FilePath = "$(Get-AllUsersDesktopFolderPath)\$Name.$Extension"
            }
            elseif ($FolderPath)
            {
                $FilePath = "$FolderPath\$Name.$Extension"
            }
            if (Test-Path -Path $FilePath -PathType Leaf)
            {
                throw "$FilePath already exists. New shortcut cannot be made here."
            }
            $Object = $ShellObject.CreateShortcut($FilePath)
            $Object.TargetPath = $TargetPath
            if ($TargetPath -notmatch '^\w{1}:\\')
            {
                $Extension = 'url'
            }
            else
            {
                $Extension = 'lnk'
                $Object.Arguments = $Arguments
                $Object.WorkingDirectory = ($TargetFilePath | Split-Path -Parent)
            }
            
            if ($PSCmdlet.ShouldProcess($FilePath,'New shortcut')) {
                Write-Verbose -Message "Creating shortcut at $FilePath using targetpath $TargetPath"
                $Object.Save()
                if (Test-Path -Path $FilePath -PathType Leaf)
                {
                    Write-Verbose -Message "Shortcut at $FilePath was successfully created"
                }
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Remove-MyService
{
    <#
    .SYNOPSIS
        This function stops and removes a Windows service
    .EXAMPLE
        Remove-MyService -Name bnpagent
    .PARAMETER ServiceName
         The service name you'd like to stop and remove
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Get-Service -Name $_ -ErrorAction 'SilentlyContinue' })]
        [string]$Name
    )
    process
    {
        try
        {
            $ServicesToRemove = Get-Service $Name
            if (-not $ServicesToRemove)
            {
                Write-Log -Message "-No services to be removed found..."
            }
            else
            {
                foreach ($Service in $ServicesToRemove)
                {
                    try
                    {
                        Write-Log -Message "-Found service $($Service.DisplayName)."
                        if ($Service.Status -ne 'Stopped')
                        {
                            Write-Log -Message "-Service $($Service.Displayname) is not stopped."
                            Stop-Service $Service
                        }
                        else
                        {
                            Write-Log -Message "-Service $($Service.Displayname) is already stopped."
                        }
                        Write-Log -Message "-Attempting to remove service $($Service.DisplayName)..."
                        $WmiService = Get-WmiObject -Class Win32_Service -Filter "Name='$($Service.ServiceName)'" -ErrorAction 'SilentlyContinue' -ErrorVariable WMIError
                        if ($WmiError)
                        {
                            Write-Log -Message "-Unable to remove service $($Service.DisplayName). WMI query errored with `"$($WmiError.Exception.Message)`"" -LogLevel '2'
                        }
                        else
                        {
                            $DeleteService = $WmiService.Delete()
                            if ($DeleteService.ReturnValue -ne 0)
                            {
                                ## Delete method error codes http://msdn.microsoft.com/en-us/library/aa389960(v=vs.85).aspx
                                Write-Log -Message "-Service $($Service.DisplayName) failed to remove. Delete error code was $($DeleteService.ReturnValue).." -LogLevel '2'
                            }
                            else
                            {
                                Write-Log -Message "-Service $($Service.DisplayName) successfully removed..."
                            }
                        }
                    }
                    catch
                    {
                        Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
                        $false
                    }
                }
            }
            
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}