Delete what triggers eventID 10

Back in the old days of Vista SP1, Microsoft introducted a permanent WMI event that triggers an event in the logs whenever your processor reaches 99% of usage. It’s a good idea but it’s unfortunately written as an error and catched by the administrative view filter.
eventID 10

The problem is that it’s a false positive that Microsoft reintroduced in Windows 7 SP1 😦
Microsoft still only gives us a workaround based on an old fashioned vbscript.

So, here’s how to achieve the same thing as the vbscript by using powershell:

# 
# Delete the eventID 10
#
# http://support.microsoft.com/kb/950375

$obj1  = @(Get-WmiObject -Namespace "root\subscription"  -Query "Select * FROM __EventFilter WHERE Name='BVTFilter'" -ErrorAction SilentlyContinue)
$obj2 = @(Get-WmiObject -Namespace "root\subscription"  -Query "Associators Of {__EventFilter.Name='BVTFilter'} WHERE AssocClass=__FilterToConsumerBinding" -ErrorAction SilentlyContinue)
if ($obj2.Count -eq 0)
{
    Write-Warning -Message "obj2 not found"
} else {

    foreach ($obj in $obj2)
    {
        try 
        {
            $obj.delete()
        }
        catch
        {
            Write-Warning -Message "Failed to delete $obj"
            $_.Exception.ErrorCode 
            $_.CategoryInfo.Reason
            $_.Exception.Message
        }
    }
}

$obj3 = @(Get-WmiObject -Namespace "root\subscription"  -Query "References Of {__EventFilter.Name='BVTFilter'} WHERE ResultClass=__FilterToConsumerBinding" -ErrorAction SilentlyContinue)
if ($obj3.Count -eq 0)
{
    Write-Warning -Message "obj3 not found"
} else {
    foreach ($obj in $obj3)
    {
        try 
        {
            $obj.delete()
        }
        catch
        {
            Write-Warning -Message "Failed to delete $obj"
            $_.Exception.ErrorCode 
            $_.CategoryInfo.Reason
            $_.Exception.Message
        }
    }
}

if ($obj1.Count -eq 0)
{
    Write-Warning -Message "obj1 not found"
} else {
    $obj1[0].delete()    
}

Get random passwords

As part of my daily operations tasks, I have sometimes to create random passwords for a new series of computers to be integrated in the domain.

Precisely, I have to create a list of computers and their random 10 characters long passwords that mixes upper and lower case letters compatible with a qwerty and azerty keyboard layout and that doesn’t match a word in a dictionary. The computer names and passwords should be tab separated so that it could be appended to our global passwords file a.xls in order to be readable in Excel (if necessary). Using a tab separated file also allows us to use this file in a scripted manner. It’s being parsed by a central custom script that checks if the password didn’t change on a daily basis.

So here’s the result of my quick and dirty approach for computer names from PTV800 to PTV899.

# Get a list of characters
$list = [Char[]]'bcdefhijklnoprstuvxBCDEFHIJKLNOPRSTUVX'

# Loop 
0..99 | % {
 # Get a 10 characters long random password where each characters is picked up from our list of characters
 $pw = (-join (1..10 | Foreach-Object { Get-Random $list -count 1 }))
 # Build our computername matching our naming convention
 $pcname = "PTV8" + ("{0:00}" -f $_)
 Write-Host -Object ($pcname + "`t" + $pw)
 # Append to our global tab separated file
 Write-Output -InputObject ($pcname + "`t" + $pw)  | Out-File -filepath .\a.xls -Append -NoClobber -Encoding ASCII
}

Working with system locale and user locale

Last week while working with the WDRAP tool (Risk and Health Assessment Program for Windows Desktop) from Microsoft, it complained about the system locale not being en-US (English United States).

So I started digging into this subject and found that there are many ways to get the user locale.
You can query the $host variable in your powrshell session:

$host.CurrentCulture

Or you can also simply query the $PSculture

$PSCulture
get-item variable:\PSCulture

Or you can use the following .Net object:

[System.Threading.Thread]::CurrentThread.CurrentCulture

You could also query the registry:

(Get-ItemProperty 'HKCU:\Control Panel\International').Locale
(Get-ItemProperty 'HKCU:\Control Panel\International').LocaleName

user locale

But, the user locale isn’t what I was looking for. The only way I found to query the System locale is by using WMI:

(Get-WmiObject Win32_OperatingSystem).locale

The WMI query returns a system string that is actually the hexadecimal number of the system locale. I was close to the result and finally used the System.Globalization.CultureInfo .Net object preloaded in the session.

The tip of the week page about Formatting Numbers and Dates Using the CultureInfo Object has a link to the .net object system.globalization.cultureinfo.

Here is how to use it

[System.Globalization.CultureInfo]("en-US")
# The object accepts also a decimal number or a decimal number written in hexadecimal:
[System.Globalization.CultureInfo](1033)
[System.Globalization.CultureInfo](0x409)

Now, I’ve just got to convert the system.string to either a decimal number or a decimal number written in hexadecimal like this:

# Using decimal number written in hexadecimal
[System.Globalization.CultureInfo]([int]("0x" + (Get-WmiObject Win32_OperatingSystem).locale))
# Using a decimal number, i.e, a system.int32
[System.Globalization.CultureInfo]([Convert]::ToInt32((Get-WmiObject Win32_OperatingSystem).locale,16))

Here are additional pages that could be as well useful resources:

This great page speaks also about the system.globalization.cultureinfo and much more:
http://powershell.com/cs/blogs/ebook/archive/2009/03/08/chapter-6-using-objects.aspx

This one shows how to Convert decimal to binary to hexadecimal and vice versa

This page explains precisely what Locale Names are.

And finally this page shows the full list of hexadecimal values and their Locale ID displayname: http://msdn.microsoft.com/en-us/library/cc233968.aspx

Get-SysinternalsTools

I’ve read the following blog post about popular scripts http://blogs.technet.com/b/heyscriptingguy/archive/2012/01/03/top-ten-scripting-wife-blogs-of-2011-show-powershell-skills.aspx and decided last week-end to update my old down_sys.bat file.
I used it to download all files from http://live.sysinternals.com using wget with proxy settings into a folder called live.sysinternals.com. It finally compares files in my current path and produces the following output.
old down_sys.bat file output
Can’t wait any further, so here’s my version of Get-SysinternalsTools.ps1 that uses the same logic:

#Requires -Version 3.0

<#
    
.SYNOPSIS    
    Download sysinternals tools
   
.DESCRIPTION  
    Download all sysinternal tools to .\live.sysinternals.com directory and update previous files located in current path

.PARAMETER Proxy
    Set the proxy address to use to download
     
.NOTES    
    Name: Get-SysinternalsTools
    Author: Emin Atac
    DateCreated: 14/01/2012
     
.LINK    
    https://p0w3rsh3ll.wordpress.com
     
.EXAMPLE
    .\Get-SysinternalsTools
    Download all the sysinternals tools without a proxy

.EXAMPLE
    .\Get-SysinternalsTools -proxy "http://my.internal.proxy.address"
    Download all the sysinternals tools with a proxy

#>

param
(
[parameter(Mandatory=$false)][System.URI]$Proxy=$null,
[parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$ProxyCredential=$null
)
$otherparams = @{}
if ($proxy)
{
    $otherparams += @{Proxy = $Proxy}
    if ($ProxyCredential)
    {
       $otherparams += @{ProxyCredentials = $ProxyCredential}
    }
}

# Define the download URL
$URL = "http://live.sysinternals.com"

# Make sure we can download into the current path + the "live.sysinternals.com" directory 
$targetdir = Join-path -Path (Get-item $pwd) -ChildPath "live.sysinternals.com"
if (-not(Test-path -path $targetdir))
{
    # Create directory
    try
    {
        New-Item -Path $targetdir -itemtype directory -force -ErrorAction SilentlyContinue | Out-Null
    } 
    catch
    {
        Write-Host -ForegroundColor Red -Object ("Error cannot create destination directory live.sysinternals.com into current path $pwd")
        exit 1
    }
}

# Make sure it is a directory
if ((Get-item -Path $targetdir) -isnot [System.IO.DirectoryInfo])
{
    Write-Host -ForegroundColor Red -Object ("Oops, $targetdir exits but it is not a directory")
    exit 1
}

# Here we go: get all the links from the main page
try
{
    $wr = (Invoke-WebRequest -Uri $URL -ErrorAction SilentlyContinue @otherparams)
}
catch
{
    $_
    exit 1
}

$links = $wr.Links

# Cycle through all these links
foreach ($link in $links)
{
    # Show some progress activity as it can be quiet long...
    $i++
    Write-Progress -activity "Dealing with link: $($link.href)" -status "Percent added: " -PercentComplete (($i/$links.Count)*100)

    # Make sure we avoid directories and download a file
    if ($link.href -notmatch "^/.*/$")
    {
        $content = $wrq = $null
        # Write-Host -ForegroundColor DarkCyan -Object ("Downloading $($link.InnerText)")
        
        try
        {
            $wrq = (Invoke-WebRequest -Uri ($URL+$link.href) -Method GET -ErrorAction SilentlyContinue @otherparams)
        } 
        catch
        {
            # Write-Host -ForegroundColor Red -Object ("Failed to download $($link.InnerText)")
        }
        
        if ($wrq.StatusCode -eq 200)
        {
            $content = $wrq.Content
            $encoding = $null
            # http://en.wikipedia.org/wiki/Mime_type
            # http://technet.microsoft.com/en-us/library/dd347719.aspx
            switch ($wrq.Headers['Content-Type'])
            {
                "text/plain"               { $encoding = [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]::string  ; break }
                "application/octet-stream" { $encoding = [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]::byte    ; break }
                default                    { $encoding = [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]::unknown ; break }
            } # end of switch

            # Save the content that is a system.byte array to a file
            Set-content -value $content -encoding $encoding -path ($targetdir + $link.href)
        }
    }
}

# Now loop and let us know what file is being updated and those remaining identical
Get-ChildItem $targetdir | ForEach-Object {
    if (Test-path (Join-path -path (Get-item $targetdir).PSParentPath -ChildPath $_.Name))
    {
        $newfileversion = $_.VersionInfo.Fileversion
        $oldfileversion = (Get-item (Join-path -path (Get-item $targetdir).PSParentPath -ChildPath $_.Name)).VersionInfo.Fileversion

        if ($newfileversion -ne $oldfileversion)
        {
            Copy-Item -Path $_.Fullname -Destination (Get-item $targetdir).PSParentPath -Force # -Confirm:$true
            Write-Host -ForegroundColor Green -Object ($_.Name + " updated from " + $oldfileversion + " -> " + $newfileversion)
        } else {
            Write-Host -ForegroundColor Green -Object ($_.Name + " identical")
        }
    }
} # end of foreach

The interesting thing with this script is that we can see “nested” (let’s say more than 1) write-progress bars in the console output while it downloads files. Among other interesting things, you have the splatting technique to handle script arguments and the ‘[system.URI]’ .net object being preserved that nicely handles the port specified in the URL.
system.URI

The following image shows the final output we get:
get-sysinternalstools output

I’ve also left inside the script a link to the get-content cmdlet help page where Joel -Jaykul- Bennett and Thomas Lee updated the documentation about the -encoding parameter of the set-content cmdlet. Jason Fossen also shows on this page how to use the set-content cmdlet with the -encoding parameter:
http://www.sans.org/windows-security/2010/02/11/powershell-byte-array-hex-convert.
So let me say, Thank you guys :-).

Working with GPO and Applocker

The other day I was asked to provide all certificate based applocker rules.
Actually, it turned out that the Group Policy that targets only active directory computer objects has some security permissions that prevents domain users from reading it.

I’ve been able to figure out the above by counting the total number of GPO in the domain:


Import-Module -Name "GroupPolicy"
Import-Module -Name "Applocker"

# Running as user who is not domain admin
(Get-GPO -All -Domain "FQDN.of.my.domain" ).Count
32

# Running as domain admin
(Get-GPO -All -Domain "FQDN.of.my.domain" ).Count
33

So, now that I know that I need to run powershell with domain admin credentials, I was able to export the settings of the GPO I was looking for. Here’s how I did it:

# Read the GPO and store it as an XML object
$GPO = [xml](Get-AppLockerPolicy -Ldap ("LDAP://" + (Get-GPO -Name "Computers Parameters").path) -Domain -XML

# Now, I can display only Publisher based rules for executables
($GPO.AppLockerPolicy.RuleCollection | Where-Object { $_.Type -eq "Exe"}).FilePublisherRule | ft -HideTableHeaders -AutoSize -Property Name,Action

Working with the WindowsInstaller.Installer object

I started working on converting some vbscripts proposed by Microsoft to extract the XML information of MSP files created by the OCT (Office Customization Tool). http://technet.microsoft.com/en-us/library/cc179027.aspx

As usual to start exploring the object, I simply typed at command prompt:

New-Object -ComObject WindowsInstaller.Installer | gm

I got the following output that shows how poor is the object and that there aren’t the methods I was looking for.
I’m actually looking for OpenDatabase method as it’s being used by the vbscript and it’s also referenced on this page:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa369432%28v=VS.85%29.aspx
WindowsInstaller.Installer output 1

I decided to have a look the MSDN documentation and found the following page
http://msdn.microsoft.com/en-us/library/1yece858%28v=VS.110%29.aspx

Idem, I explored the object’s methods with the Get-Member cmdlet.

New-Object System.Configuration.Install.Installer | gm

I had a better result but definitly not the methods I expected.
System.Configuration.Install.Installer output 2

Actually, I came across the following page, http://www.snowland.se/2010/02/21/read-msi-information-with-powershell and decided to extend the original WindowsInstaller.Installer COM Object but without using the Update-TypeData cmdlet and the additional get-help .ps1xml file as it should be signed,…
Moreover the help on this page, http://technet.microsoft.com/en-us/library/dd347581.aspx states that:

However, if you need to add properties or methods only to one instance of an object, use the Add-Member cmdlet.

get-help about_types.ps1xml -full

Things worked great with the following code

# Connect to Windows Installer object    
$wi = New-Object -ComObject WindowsInstaller.Installer

$codeInvokeMethod = {
    $type = $this.gettype();
    $index = $args.count -1 ;
    $methodargs=$args[1..$index]
    $type.invokeMember($args[0],[System.Reflection.BindingFlags]::InvokeMethod,$null,$this,$methodargs)
}		
$wi = $wi | Add-Member -MemberType ScriptMethod -Value $codeInvokeMethod -Name InvokeMethod -PassThru

# Open OCT patch and read the metadata stream
try
{
    # 32 = msiOpenDatabaseModePatchFile
    $wiStorage = $wi.InvokeMethod("OpenDatabase",$Path,32)
} catch {
    $_
    exit
}

Until I had to use the ReadStream method that has many parameters.
I had a the following error

Exception calling "InvokeMethod" with "4" argument(s): "Exception calling "InvokeMember" with "5" argument(s): "Type mismatch. (Exception from HRESULT: 0x80020005 (DISP_E_TYPEMISMATCH))""

The answer came actually both from this command

($wi.GetType() | Get-Member) | Where-Object Name -eq InvokeMember | fl -Property *

WindowsInstaller.installer output 3
as well as from this page:
http://stackoverflow.com/questions/5544844/how-to-call-a-complex-com-method-from-powershell

So, the solution is:

# $vw.Execute()
$vw = $vw | Add-Member -MemberType ScriptMethod -Value $codeInvokeMethod -Name InvokeMethod -PassThru
$vw.InvokeMethod("Execute")

# $rec = $vw.Fetch()
$rec = $vw.InvokeMethod("Fetch")

$codeInvokeParamProperty = {
    $type = $this.gettype();
    $index = $args.count -1 ;
    $methodargs=$args[1..$index]
    $type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,$methodargs)
}

$rec = $rec | Add-Member -MemberType ScriptMethod -Value $codeInvokeParamProperty -Name InvokeParamProperty -PassThru

If ($rec -ne $null)
{
    
    $DataSize = $rec.InvokeParamProperty("DataSize",2)

    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa371140%28v=vs.85%29.aspx
    $paramHT = @{Field = 2 ; Length = [int]$DataSize ; Format = 1}

    $sMetadata = $rec.GetType().InvokeMember("ReadStream", [System.Reflection.BindingFlags]::InvokeMethod,
                $null,  # Binder
                $rec,  # Target
                ([Object[]]($paramHT.Values)),  # Args
                $null,  # Modifiers
                $null,  # Culture
                ([String[]]($paramHT.Keys))  # NamedParameters
            )
    
} else {
    Write-Output -InputObject "No Metadata stream was found in this file: $Path"
    Exit 2
}

Get MSI packages information from the registry

I have some tiny virtual machines running Windows XP with less than 10GB on their system drive.
After a certain amount of time, the free disk space tends to get less and less as we roll out patches.

I’ve decided to track some huge msp files left behind, hidden in the %systemroot%\installer folder that have been superseded by a service pack or a more recent cumulative update.

I know that we usually get information from WMI using the Win32_product class but all the interesting information I was looking for is also written in the registry under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer.

I’ve sucessfully tested the Get-MSIZapInfo.ps1 script against Adobe Reader 9.x patches and against the Microsoft Compatibility Pack for the 2007 Office system. With these two products, I’ve been able to remove around 300MB of superseded MSP files. I’ve been able to verify that the Adobe Reader 9.x superseded patches matches the ‘Out-of-Cycle’ (OOC) patches listed on this page

#Requires -Version 2.0
 
<#
 
.SYNOPSIS    
    Get MSIinstaller related info from the registry
 
.DESCRIPTION  
    Get MSIinstaller related info from the registry
 
.PARAMETER ShowSupersededPatches
    Switch to show supersed patches of MSI products instead of default installed MSI products
 
.NOTES    
    Name: Get-MSIZapInfo
    Author: Emin Atac
    DateCreated: 02/01/2012
 
.LINK    
    https://p0w3rsh3ll.wordpress.com
 
.EXAMPLE    
    Get-MSIZapInfo.ps1 | fl -property *
    Pipe it into format-list and show all the returned properties
 
.EXAMPLE    
    Get-MSIZapInfo.ps1 | fl -property Displayname
    Pipe it into format-list and show only the displayname of MSI installed products
 
.EXAMPLE
    Get-MSIZapInfo.ps1 | Format-Custom -Depth 2
    Pipe it into format-custom to explore its depth
 
.EXAMPLE    
    .\Get-MSIZapInfo.ps1 | Select-Object -Property Displayname,Displayversion,Publisher | ft -AutoSize -HideTableHeaders    
    Select some properties and pipe it into the format-table cmdlet
 
.EXAMPLE
    .\Get-MSIZapInfo.ps1 -ShowSupersededPatches |  ft -AutoSize -Property LocalPackage,Superseded,DisplayName
    Show superseded patches of all MSI products installed and filter some of its properties using the format-table cmdlet
 
.EXAMPLE
    (.\Get-MSIZapInfo.ps1 | Where-Object -FilterScript  {$_.Displayname -match "LifeCam"}) | fl -Property *
    Find a specific MSI product installed by its displayname and show all its properties
 
.EXAMPLE
    (.\Get-MSIZapInfo.ps1  | Where-Object -FilterScript  {$_.Displayname -match "Silverlight"}).AllPatchesEverinstalled | ft -AutoSize
    Look for Silverlight and show all its patches ever installed
 
.EXAMPLE
    .\Get-MSIZapInfo.ps1 | Where-Object -FilterScript  {$_.Displayname -match "Microsoft Office" } | fl -Property Displayname,RegistryGUID,UninstallString,ConvertedGUID
 
.EXAMPLE    
    Invoke-Command -ComputerName RemoteComputername -FilePath .\Get-MSIzap.ps1    
    List all MSI products of a remote computer
 
.EXAMPLE        
    (Invoke-Command -ComputerName RemoteComupterName -FilePath .\Get-MSIzap.ps1) | % {$_.AllPatchesEverinstalled}
    List all MSI products patches ever installed on a remote computer
 
.EXAMPLE
    $all = .\Get-MSIZapInfo.ps1
    foreach ($j in $all)
    {
        if ($j.AllPatchesEverinstalled -ne $null)
        {
            $j.AllPatchesEverinstalled | Where-Object {$_.Superseded -eq $false} |  ft -AutoSize -Property LocalPackage,Superseded,DisplayName 
 
        }
    }    
    Show non supersed patches of all installed MSI products
 
#>
 
param
(
[parameter(Mandatory=$false,Position=0)][switch]$ShowSupersededPatches
)
 
Function Test-MatchFromHashTable
{
param(
[parameter(Mandatory=$true,Position=0)][system.array]$array = $null,
[parameter(Mandatory=$true,Position=1)][system.string]$testkey = $null,
[parameter(Mandatory=$true,Position=2)][system.string]$string = $null
)
 if ($string -ne $null)
 {
    $occurences = @()
    foreach ($i in $array)
    {
        if ($i.$testkey -match $string)
        {
        $occurences += $i
        }
    }
    if ($occurences -ne $null)
    {
     return $true
    } else {
     return $false
    }
 } else {
    Write-Host -ForegroundColor Red -Object "Cannot use this function to test against a null string"
    break
 }    
} # end of function
 
Function Get-MatchFromHashTable
{
param(
[parameter(Mandatory=$true,Position=0)][system.array]$array = $null,
[parameter(Mandatory=$true,Position=1)][system.string]$testkey = $null,
[parameter(Mandatory=$true,Position=2)][system.string]$string = $null
)
    $occurences = @()
    foreach ($i in $array)
    {
        if ($i.$testkey -match $string)
        {
        $occurences += $i
        }
    }
    return $occurences
} # end of function
 
function ConvertTo-NtAccount
{
<#
 
.SYNOPSIS    
    Translate a SID to its displayname
 
.DESCRIPTION  
    Translate a SID to its displayname
 
.PARAMETER Sid
    Provide a SID
 
.NOTES    
    Name: ConvertTo-NtAccount
    Author: thepowershellguy
 
.LINK    
    http://thepowershellguy.com/blogs/posh/archive/2007/01/23/powershell-converting-accountname-to-sid-and-vice-versa.aspx
 
.EXAMPLE
    ConvertTo-NtAccount S-1-1-0
    Convert a well-known SID to its displayname
 
#>
 
param(
[parameter(Mandatory=$true,Position=0)][system.string]$Sid = $null
)
 begin
 {
    $obj = new-object system.security.principal.securityidentifier($sid)
 }
 process
 {
    try
    {
        $obj.translate([system.security.principal.ntaccount])
    }
    catch
    {
        # To remove the silent fail, uncomment next line
        # $_
    }
 }
 end
 {
 }
}
 
function ConvertTo-Sid
{
<#
 
.SYNOPSIS    
    Translate a user name to a SID
 
.DESCRIPTION  
    Translate a user name to a SID
 
.PARAMETER Sid
    Provide a username
 
.NOTES    
    Name: ConvertTo-Sid
    Author: thepowershellguy
 
.LINK    
    http://thepowershellguy.com/blogs/posh/archive/2007/01/23/powershell-converting-accountname-to-sid-and-vice-versa.aspx
 
.EXAMPLE
    (ConvertTo-Sid "Domain\Administrator").Value
    Get the SID of the Active Directory Domain admininstrator
 
.EXAMPLE
    ConvertTo-Sid "Administrator"
    Get the SID of the local administrator account
#>
 
param(
[parameter(Mandatory=$true,Position=0)][system.string]$NtAccount = $null
)
 begin
 {
    $obj = new-object system.security.principal.NtAccount($NTaccount)
 } 
 process
 {
    try
    {
        $obj.translate([system.security.principal.securityidentifier])
    }
    catch
    {
        # To remove the silent fail, uncomment next line
        # $_
    }
 } 
 end
 {
 }
}
 
Function Convert-RegistryGUID
{
param(
    [parameter(Mandatory=$true)]
    [ValidateLength(32,32)]
    [system.string]
    $string
)
 
    $string | % { 
        -join (
            -join ( $_.Substring(0,8).ToCharArray()[(($_.Substring(0,8).ToCharArray()).Count - 1)..0]),"-",
            -join ( $_.Substring(8,4).ToCharArray()[(($_.Substring(8,4).ToCharArray()).Count - 1)..0]),"-",
            -join ( $_.Substring(12,4).ToCharArray()[(($_.Substring(12,4).ToCharArray()).Count - 1)..0]),"-",
            -join ( ($_.Substring(16,4) -split "(?<=\G.{2})",4 | % { $_.ToCharArray()[($_.ToCharArray().Count - 1)..0] })),"-",
            -join ( ($_.Substring(20,12) -split "(?<=\G.{2})",6 | % { $_.ToCharArray()[($_.ToCharArray().Count - 1)..0] }))
        )
    }
 
}
# Convert-RegistryGUID -String "00004109110000000000000000F01FEC"
 
 
# Main: read info in the registry and populate array with object that have the properties we are looking for
 
if (Test-Path HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData)
{
    $root = Get-Childitem HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData
 
    # Initialize the patches and products global array
    $patchesar = @()
    $prodcutssar = @()
 
    # Cycle through all SIDs
    foreach ($i in $root)
    {
        Write-Verbose -Message "Dealing with $($i.Name)" #-Verbose:$true
 
        # Convert the SID to an account name
        $UserWhoInstalled = ConvertTo-NtAccount $i.PSChildName
 
        # Build the main subkey
        $subkey = Join-Path -Path $i.PSParentPath -ChildPath $i.PSChildName
 
 
        # Get the whole list of patches
        if (Test-Path "$subkey\Patches")
        {
            $patches =  Get-Childitem "$subkey\Patches" 
            foreach ($k in $patches)
            {
                $patchkey = Join-Path -Path $k.PSParentPath -ChildPath $k.PSChildName
 
                $PatchObject = New-Object -TypeName PSObject -Property @{
                    RegistryGUID = $k.PSChildName
                    InstalledBy = $UserWhoInstalled
                    LocalPackage = (Get-ItemProperty -Path $patchkey).LocalPackage
                    }
 
                # Add our object to the global array
                $patchesar += $PatchObject
            }
        }
 
        # Get the list of products and their properties
        if (Test-Path "$subkey\Products")
        {
            $products = Get-Childitem "$subkey\Products"
 
            foreach ($j in $products)
            {
                Write-Verbose -Message "Dealing with $($j.Name)" # -Verbose:$true
 
                # Build the subkey and gather all properties
                $productkey = Join-Path -Path $j.PSParentPath -ChildPath $j.PSChildName
                $productInstallProperties = Get-ItemProperty -Path $productkey\InstallProperties
 
                # Populate our object with all the properties we are interested in
                $ProductObj = New-Object -TypeName PSObject -Property @{  
                    InstalledBy = $UserWhoInstalled
                    RegistryGUID = $j.PSChildName
                    Displayname = $productInstallProperties.DisplayName
                    Publisher = $productInstallProperties.Publisher
                    DisplayVersion = $productInstallProperties.DisplayVersion
                    InstallDate = ([System.DateTime]::ParseExact($productInstallProperties.InstallDate,"yyyyMMdd",[System.Globalization.CultureInfo]::InvariantCulture))
                    LocalPackage = $productInstallProperties.LocalPackage
                    UninstallString = ($productInstallProperties.UninstallString -replace "msiexec\.exe\s/[IX]{1}","")
                    ConvertedGUID = (Convert-RegistryGUID -String $j.PSChildName)
                    }
 
                # Get the list of patches GUID for a product
 
                # Build the array of current patches w/o superseded patches from the Allpatches value found in the registry
                $AllpatchesValuear = @()
                $AllpatchesValue = (Get-ItemProperty -Path $productkey\Patches).Allpatches -split "`n"
                for ($i = 0 ; $i -le ($AllpatchesValue.Count - 1) ; $i++)
                {
                    $AllpatchesValuear += $AllpatchesValue[$i]
                }
 
                # Cycle to all patches found by reading subkeys
                $Allproductpatches = Get-Childitem "$productkey\Patches"
                $AllpatchesInstalled = @()
                if ($Allproductpatches -ne $null)
                {
                    foreach ($l in $Allproductpatches)
                    {
                        $patchesubkey = Join-Path -Path $l.PSParentPath -ChildPath $l.PSChildName
                        $patchproperties = Get-ItemProperty -Path $patchesubkey
 
                        $PatchObj = New-Object -TypeName PSObject -Property @{            
                            RegistryGUID = $l.PSChildName
                            Displayname =$patchproperties.DisplayName
                            InstallDate = ([System.DateTime]::ParseExact($patchproperties.Installed,"yyyyMMdd",[System.Globalization.CultureInfo]::InvariantCulture))
                            }
 
                        # Prepare to define a supersedence property
                        switch ($patchproperties.State)
                        {
                            2 { $Superseded = $true}
                            1 { $Superseded = $false}
                            default { $Superseded = "Unknown"}
                        }                    
                        $PatchObj | add-member Noteproperty -Name Superseded -Value $Superseded
 
                        if (Test-MatchFromHashTable -array $patchesar -testkey RegistryGUID -string $PatchObj.RegistryGUID)
                        {
                           $PatchObj | add-member Noteproperty -Name LocalPackage -Value (Get-MatchFromHashTable -array $patchesar -testkey RegistryGUID -string $PatchObj.RegistryGUID).LocalPackage
                        }
                        # Add to array
                        $AllpatchesInstalled += $PatchObj
                    }
                }
 
                $ProductObj | add-member Noteproperty -Name AllPatchesEverinstalled -Value $AllpatchesInstalled
                $ProductObj | add-member Noteproperty -Name AllCurrentPatchesinstalled -Value $AllpatchesValuear
 
                # Add our object to the global array
                $prodcutssar += $ProductObj
 
            } # end of foreach
        } # end of if test-path products
    } # end of foreach root
} # end of test-path
 
if ($ShowSupersededPatches)
{
    # Superseded patches
    $Supersededpatches = @()
    foreach ($j in $prodcutssar)
    {
        if ($j.AllPatchesEverinstalled -ne $null)
        {
            $Supersededpatches += ($j.AllPatchesEverinstalled | Where-Object {$_.Superseded -eq $true})
 
        }
    }
    return $Supersededpatches
} else {
    return $prodcutssar
}