Script Browser for Windows PowerShell ISE

The Script Browser for Windows PowerShell ISE has been announced yesterday on the PowerShell Team blog and was actually released the day before on the download center

I wanted to give it a try before the announcement but I was too enthusiast and failed to meet the requirements.
To save your time, here are the requirements. You need:

  1. Microsoft .NET Framework 4 (required by PowerShell 3.0)
  2. Windows Management Framework 3.0
  3. Microsoft .NET Framework version 4.5 or a later version (required by ScriptBrowser.dll)

I missed the .Net 4.5 and got the following error:
Add-Type : Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.

To get the above display, I modified the code of the Powershell ISE profile ($profile = “$env:USERPROFILE\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1″) like this:

#Script Browser Begin
try {
	Add-Type -Path 'C:\Program Files (x86)\Microsoft Corporation\Microsoft Script Browser\System.Windows.Interactivity.dll' -ErrorAction Stop
	Add-Type -Path 'C:\Program Files (x86)\Microsoft Corporation\Microsoft Script Browser\ScriptBrowser.dll' -ErrorAction Stop
	Add-Type -Path 'C:\Program Files (x86)\Microsoft Corporation\Microsoft Script Browser\BestPractices.dll' -ErrorAction Stop
	$scriptBrowser = $psISE.CurrentPowerShellTab.VerticalAddOnTools.Add('Script Browser', [ScriptExplorer.Views.MainView], $true)
	$scriptAnalyzer = $psISE.CurrentPowerShellTab.VerticalAddOnTools.Add('Script Analyzer', [BestPractices.Views.BestPracticesView], $true)
	$psISE.CurrentPowerShellTab.VisibleVerticalAddOnTools.SelectedAddOnTool = $scriptBrowser
#Script Browser End
} catch {
	Write-Warning -Message "Failed because $($_.Exception.message)"

To fix the error, I installed Microsoft .NET Framework 4.5.1 (Offline Installer) that

is a highly compatible, in-place update to the Microsoft .NET Framework 4 and the Microsoft .NET Framework 4.5.

Then I read a second time the page and complied with the ‘install instruction’

  1. Start Windows PowerShell with the “Run as administrator” option.
  2. Run this command: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

but I run into a second issue:
As you can see on the following screenshot, there’s a message saying Network error, please check your internet connection and proxy settings in the newly loaded ‘script browser’ tab.

Worse, the Update-Help and Invoke-WebRequest don’t run anymore although a procmon trace shows that the HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\ProxySettingsPerUser isn’t found and that my proxy settings are being read and used correctly from the Current User registry hive.
I used the following code snippet I posted on this blog post when investigating another proxy issue in order to determine what proxy settings the ISE was reading from the HKCU hive.

-join (
(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections').DefaultConnectionSettings |
Foreach { [char][int]"$_"}

Then I removed the newly created Microsoft.PowerShellISE_profile.ps1 responsible for displaying the script browser tab and confirmed that Update-Help and Invoke-WebRequest work without any issue.

Then I loaded manually the script browser and again it had the same issue.

After that I clicked on the option symbol I’ve highlighted

…and as soon as I’ve set the ‘use Internet Explorer’ settings, everything worked :-)

You can view the settings with:


That said. You must also be warned. Don’t try to set properties on ScriptExplorer.Properties.Settings default instance although it appears to be possible.
You may end up with inconsistent settings and the script explorer may not launch anymore.

If you mess with the settings of the ScriptExplorer.Properties.Settings default instance, you might need to delete the user.config while the ISE is running as it gets recreated when the ISE is closed.

Get-ChildItem "$env:userprofile\AppData\Local\Microsoft_Corporation\powershell_ise*" -Include user.config -Recurse -Force -ErrorAction SilentlyContinue | 
Remove-Item -Verbose

The settings of the script explorer are stored in the user.config file and can be also viewed like this:

$xml = ([xml](Get-Content (Get-ChildItem "$env:userprofile\AppData\Local\Microsoft_Corporation\powershell_ise*" -Include user.config -Recurse -Force -ErrorAction SilentlyContinue)))

Follow-up on downloading Windows Assessment and Deployment Kit (Windows ADK) 8.1

I proposed last year two scripts to download the latest Windows Assessment and Deployment Kit (ADK) files using BITS and PowerShell :-)

Today is April 8, 2014 and it’s the perfect time to update the script.

But let’s first see how I built the DATA blocks in the function.

I first launched the webinstaller of the ADK and told him to download files into C:\ADK.
Then I used PowerShell to extract the DATA using the following code.

# First test the regular expression with the built-in select-string cmdlet
Get-Content 'C:\adk\Windows Assessment and Deployment Kit for Windows 8.1_20140403114657.log' | 
Select-String -Pattern "Acquiring\spackage:\s(?<Name1>.*),\spayload:\s(?<Name2>.*),\sdownload\sfrom:\s(?<URL>.*)$"

# Extract URL and file names from the log file and send it through the pipeline
$ADKFiles = (
Get-Content "C:\adk\Windows Assessment and Deployment Kit for Windows 8.1_20140403114657.log" -ReadCount 1 | ForEach-Object {
    ([regex]'Acquiring\spackage:\s(?<Name1>.*),\spayload:\s(?<Name2>.*),\sdownload\sfrom:\s(?<URL>.*)$').Matches($_) | ForEach-Object {
            URL = @($_.Groups)[-1].Value ;
            FileName = ([URI]@($_.Groups)[-1].Value).Segments[-1] -replace '%20'," "

# Let me know how many files are downloaded
Write-Verbose -Message ("There are {0} files in the ADK toolkit to download" -f $Adkfiles.Count) -Verbose

Write-Verbose -Message (
    "There are {0} files in the ADK toolkit from ../adk/Installers" -f ($Adkfiles| Where URL -match "adk/Installers/").Count
) -Verbose

Write-Verbose -Message (
    "There are {0} files in the ADK toolkit from ../adk/Patches" -f ($Adkfiles| Where URL -notmatch "adk/Installers/").Count
) -Verbose

# Construct the first DATA block
$count = 0
($Adkfiles| Where URL -match "adk/Installers/") | ForEach-Object {
 '{0}={1}' -f $count,$_.FileName
# Construct the second DATA block
$count = 0
($Adkfiles| Where URL -notmatch "adk/Installers/") | ForEach-Object {
 '{0}={1}' -f $count,$_.FileName

Then I copied the console output you can see above into the appropriate DATA blocks and updated manually the version string from 8.100.26020 to 8.100.26629

#Requires -Version 4
#Requires -RunAsAdministrator 
Function Get-ADKFiles {
Begin {
    $HT = @{}
    $HT += @{ ErrorAction = 'Stop'}
    # Validate target folder
    try {
        Get-Item $TargetFolder @HT | Out-Null
    } catch {
        Write-Warning -Message "The target folder specified as parameter does not exist"
Process {
    $adkGenericURL = (Invoke-WebRequest -Uri -MaximumRedirection 0 -ErrorAction SilentlyContinue)

    # 302 = redirect as moved temporarily
    if ($adkGenericURL.StatusCode -eq 302) {
        # Currently set to
        $MainURL = $adkGenericURL.Headers.Location
        $InstallerURLs = DATA {
            ConvertFrom-StringData @'
                0=Toolkit Documentation-x86_en-us.msi
                6=Application Compatibility Toolkit-x86_en-us.msi
                11=Microsoft Compatibility Monitor-x86_en-us.msi
                12=Application Compatibility Toolkit-x64_en-us.msi
                14=Microsoft Compatibility Monitor-x86_en-us.msi
                15=Windows Deployment Tools-x86_en-us.msi
                45=Windows System Image Manager on amd64-x86_en-us.msi
                46=Windows System Image Manager on x86-x86_en-us.msi
                47=Windows Deployment Customizations-x86_en-us.msi
                56=Windows PE x86 x64-x86_en-us.msi
                64=Windows PE x86 x64 wims-x86_en-us.msi
                67=User State Migration Tool-x86_en-us.msi
                70=Volume Activation Management Tool-x86_en-us.msi
                74=WPT Redistributables-x86_en-us.msi
                78=Windows Assessment Toolkit-x86_en-us.msi
                82=Windows Assessment Toolkit (AMD64 Architecture Specific)-x86_en-us.msi
                83=Windows Assessment Toolkit (X86 Architecture Specific)-x86_en-us.msi
                84=Assessments on Client-x86_en-us.msi
                146=Windows Assessment Services-x86_en-us.msi
                150=Windows Assessment Services - Client (Server SKU)-x86_en-us.msi
                153=Windows Assessment Services - Client (AMD64 Architecture Specific, Server SKU)-x86_en-us.msi
                154=Assessments on Server-x86_en-us.msi
                156=Windows Assessment Services - Client (Client SKU)-x86_en-us.msi
                157=Windows Assessment Services - Client (X86 Architecture Specific, Client SKU)-x86_en-us.msi
                158=Windows Assessment Services - Client (AMD64 Architecture Specific, Client SKU)-x86_en-us.msi
                159=Kits Configuration Installer-x86_en-us.msi
        $PatchesURLs = DATA {
            ConvertFrom-StringData @'
                0=Toolkit Documentation-x86_en-us.msp
                1=Application Compatibility Toolkit-x86_en-us.msp
                2=Application Compatibility Toolkit-x64_en-us.msp
                3=Windows Deployment Tools-x86_en-us.msp
                4=Windows System Image Manager on amd64-x86_en-us.msp
                5=Windows System Image Manager on x86-x86_en-us.msp
                6=Windows PE x86 x64-x86_en-us.msp
                7=User State Migration Tool-x86_en-us.msp
                8=Volume Activation Management Tool-x86_en-us.msp
                11=WPT Redistributables-x86_en-us.msp
                12=Windows Assessment Toolkit-x86_en-us.msp
                13=Assessments on Client-x86_en-us.msp
                14=Windows Assessment Services-x86_en-us.msp
                15=Windows Assessment Services - Client (Server SKU)-x86_en-us.msp
                16=Assessments on Server-x86_en-us.msp
                17=Windows Assessment Services - Client (Client SKU)-x86_en-us.msp
        "Installers","Patches\8.100.26629" | ForEach-Object -Process {
            # Create target folders if required as BIT doesn't accept missing folders
            If (-not(Test-Path (Join-Path -Path $TargetFolder -ChildPath $_))) {
                try {
                    New-Item -Path (Join-Path -Path $TargetFolder -ChildPath $_) -ItemType Directory -Force @HT
                } catch {
                    Write-Warning -Message "Failed to create folder $($TargetFolder)/$_"
        # Get adksetup.exe
        Invoke-WebRequest -Uri "$($MainURL)adksetup.exe" -OutFile  "$($TargetFolder)\adksetup.exe"
        # Create a job that will downlad our first file
        $job = Start-BitsTransfer -Suspended -Source "$($MainURL)Installers/$($InstallerURLs['0'])" -Asynchronous -Destination (Join-Path -Path $TargetFolder -ChildPath ("Installers/$($InstallerURLs['0'])")) 
        # Downlod installers
        For ($i = 1 ; $i -lt $InstallerURLs.Count ; $i++) {
            $URL = $Destination = $null
            $URL = "$($MainURL)Installers/$($InstallerURLs[$i.ToString()])"
            $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath Installers) -ChildPath (([URI]$URL).Segments[-1] -replace '%20'," ")
            # Add-BitsFile
            $newjob = Add-BitsFile -BitsJob $job -Source  $URL -Destination $Destination
            Write-Progress -Activity "Adding file $($newjob.FilesTotal)" -Status "Percent completed: " -PercentComplete (($newjob.FilesTotal)*100/($InstallerURLs.Count))
        # Donwload Patches
        For ($i = 0 ; $i -lt $PatchesURLs.Count ; $i++) {
            $URL = $Destination = $null
            $URL = "$($MainURL)Patches/8.100.26629/$($PatchesURLs[$i.ToString()])"
            $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath "Patches/8.100.26629") -ChildPath (([URI]$URL).Segments[-1] -replace '%20'," ")
            # Add-BitsFile
            $newjob = Add-BitsFile -BitsJob $job -Source  $URL -Destination $Destination
        # Begin the download and show us the job
        Resume-BitsTransfer  -BitsJob $job -Asynchronous
        while ($job.JobState -in @('Connecting','Transferring','Queued')) {
            Write-Progress -activity "Downloading ADK files" -Status "Percent completed: " -PercentComplete ($job.BytesTransferred*100/$job.BytesTotal)
        Switch($job.JobState) {
         "Transferred" {
            Complete-BitsTransfer -BitsJob $job
         "Error" {
            # List the errors.
            $job | Format-List
        default {
            # Perform corrective action.
End {}

Then I tested the function like this:

  1. I created the target folder
  2. If you don’t create it and try to launch the function, you’ll get the following warning:

  3. I dotsourced the script
  4. I launched the download

  5. I compared the downloaded files made with the webinstaller and my function.

The following links are still valid to obtain the:

Analysing files with public API

I’ve sometimes some forensics tasks to do at work. A member of the network team gave me a USB stick with the incoming mail server traffic.
My task was to get files’ thumbprints and query using its public API to see if these file hashes are known. I’d have used the private API of virustotal if I’d the right to upload the file to

Before plugging-in the USB stick, I uninstalled my antivirus and then disabled Windows Defender.

I recycled some obsolete code I already published in January 2013 that you can find on this page.

One of the problems with my previous code was that the URL it used is not valid anymore. Now virustotal supports multilanguage and the correct URL is

The second problem with my code was that the core regular expression is obsolete and doesn’t match the updated HTML code returned by a search.

To fix the code, I had to shift back to the methodology I used in the first place in 2013 before coming up with the PowerShell code.
In other words, I had to analyse the web queries (hearders and redirection) and their HTML code.
I submitted manually both a known safe SHA2 thumbprint of a file, a24f400c4fc6b7d4085f9e264f6d3f70c13d678c0e34b3e31f9163cf10e423ec , a known malware thumbprint 7e8bb57ad97ace3aa4a8f3ecaf5538e84b58a06e16569f3f60f190fa3e83f80b and a totally unknown thumbprint that has never been submitted to
…and started a network capture in IE11

Then with the above SHA2 checksum, I did:

$res = (Invoke-WebRequest -Uri '' -Method Post -Body "query=$hash" -MaximumRedirection 0 -ErrorAction SilentlyContinue)            
$page = Invoke-WebRequest -Method GET -Uri $res.Headers.Location -ErrorAction SilentlyContinue -MaximumRedirection 0

and I started to use the excellent Get-Matches function provided by Dr. Tobias Weltner:

I started to find a regular expression that matches each case:

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass=ltr>(?<ID>.*)\s</TD>'

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass="ltr\stext\-green"><I\stitle="(?<ID>.*)"\sclass=icon\-ok\-sign\sdata\-toggle="tooltip"></I></TD>'

for the following HTML code:

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass="ltr\stext-red">(?<ID>.*)\s</TD>'

for the following HTML code:

The last step consisted in merging all the 3 regular expressions into a single one like this:

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass=("?)ltr(>|\stext\-green"><I\stitle="|\stext-red">)(?<ID>.*)("\sclass=icon\-ok\-sign\sdata\-toggle="tooltip"></I></TD>|\s</TD>)'

Here’s the full code I used to obtain at the end an Excel file containing the results:

#Requires -version 4.0

$allfiles = @()
# We first clear all errors in the automatic variable
# We capture all files and let error happen silently and being logged into the $error automatic variable
$allfiles = Get-ChildItem -Path C:\ZIP -Recurse -Force -Include * -File -ErrorAction SilentlyContinue

# Let us know what happen
$Error | Where { $_.CategoryInfo.Reason -eq "PathTooLongException" } | ForEach-Object -Begin{
    Write-Warning -Message "The following folders contain a file longer than 260 characters"
    # Get-ChildItem : The specified path, file name, or both are too long. 
    # The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
} -Process {
Write-Verbose -Message ('There {0} other type of errors' -f ($Error | Where { $_.CategoryInfo.Reason -ne "PathTooLongException" }).Count) -Verbose
Write-Verbose -Message ("There's a total of {0} files" -f $allfiles.Count) -Verbose

# Show extensions by occurence
$allfiles | Group -NoElement -Property Extension | Sort -Property Count -Descending

Start-Sleep -Seconds 1

$totalzip = ($allfiles | Where { $_.Extension -eq '.zip' }).Count
$filecount = 0

# Select only files with a zip extension
$results = @()
$allfiles | Where { $_.Extension -eq '.zip' } | 
ForEach-Object {

    $hash = $res = $page = $checksum = $obj = $outtext = $null
    Start-Sleep -Milliseconds (Get-Random -Maximum 750 -Minimum 500)

    $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash
    Write-Verbose -Message ('Searching file {2}/{3} on {0} with sha256 {1}' -f $_.FullName,$hash,$filecount,$totalzip) -Verbose
    # Append a SHA256
    $_ | Add-Member -MemberType NoteProperty -Name SHA256 -Value $hash -Force

    # Search virustotal by SHA256
    $res = (Invoke-WebRequest -Uri '' -Method Post -Body "query=$hash" -MaximumRedirection 0 -ErrorAction SilentlyContinue)

    if ($res.StatusCode -eq 302 ) {
        if ($res.Headers.Location) {
            try {
                $page = Invoke-WebRequest -Method GET -Uri $res.Headers.Location -ErrorAction SilentlyContinue -MaximumRedirection 0
            } catch {
                Write-Warning -Message "The request on $($res.Headers.Location) returned $($_.Exception.Message)"
            if ($page.Headers.Location -notmatch "file/not/found/") {
                try {
                    $obj = New-Object -TypeName PSObject
                    $outtext = ($page.AllElements | Where { ($_.TagName -eq 'TBODY') -and ($_.outerHTML -match "$hash") -and ($_.outerText -match "Detection\sratio") }).OuterText
                    if ($outtext) {
                        $outtext -split "`n" |  ForEach-Object {
                            if ($_ -match ":") {
                            $obj | Add-Member -MemberType NoteProperty -Name ($_ -split ":")[0] -Value (-join($_ -split ":" )[1..($_ -split ":" ).count]) -Force
                            } else {
                                Write-Warning -Message "Mismatch with ':'"
                        # Analysis tab
                        $count = 0
                        $analysisar = @()
                        $AVName = $DetectionDate = $DetectionRate = $null
                        $page.AllElements.FindById('antivirus-results').outerHTML) | ForEach-Object -Process {
                            switch ((@($_.Groups))[-1]) {
                                {$_ -match '\d{8}'} { $DetectionDate = $_ ; break}
                                {$_ -match '(^\-$|^File\snot\sdetected$)'}{ $DetectionRate = '-' ; break }
                                {$_ -match '.*'} {
                                    if ($count -eq 1) {
                                        $AVName = $_
                                    } else {
                                        $DetectionRate = $_
                                    ; break
                            if ($count -eq 3) {
                                $count = 0
                                $analysisar += New-Object -TypeName PSObject -Property @{
                                    Update = $DetectionDate
                                    Result = $DetectionRate
                                    Antivirus = $AVName
                        $obj | Add-Member -MemberType NoteProperty -Name Analysis -Value $analysisar -Force
                        $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value $obj -Force
                    } else {
                        # Write-Warning -Message "$outtext # is because the file has probably been never submitted"
                        # it shouldn't happen but... who knows
                        $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value ([string]::Empty) -Force
                } catch {
            } else {
                Write-Warning -Message "the file was not found"
                $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value "Unknown by VT" -Force
        } else {
            Write-Warning -Message "the location in the header is empty"
            $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value "Header empty issue" -Force
    } else {
        Write-Warning -Message "the page returned a $($res.StatusCode) status code"
        $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value "Status code issue" -Force
    $results += $_

Write-Verbose -Message ("There's a total of {0} results" -f $results.Count) -Verbose

Write-Verbose -Message ("There's a total of {0} unknown files" -f (
    $results | Where 'VTResults' -eq "Unknown by VT").Count
) -Verbose

# Export results to CSV

# First unknown files
($results | Where 'VTResults' -eq "Unknown by VT") | 
Select Name,FullName,SHA256,
    @{l='Ratio';e={'Unknown by VT'}},
    @{l='MalwareName';e={[string]::Empty}} | 
Export-Csv -Path "$($env:USERPROFILE)\Documents\VT.Zipfiles.analysis.csv" -Encoding UTF8  -NoTypeInformation -Delimiter ";"

# Then export files that are identified as malware
Write-Verbose -Message ("There's a total of {0} known files" -f (
    $results | Where 'VTResults' -notin @("Unknown by VT","Header empty issue","Status code issue")).Count
) -Verbose

($results | Where 'VTResults' -notin @("Unknown by VT","Header empty issue","Status code issue")) |
Select Name,FullName,SHA256,
        $_.VTResults.'Detection Ratio' -replace '\s',''
        ($_.VTResults.Analysis | Where { $_.Result -notmatch "^\-$"}).Result -as [string[]]
    }} |
Export-Csv -Path "$($env:USERPROFILE)\Documents\VT.Zipfiles.analysis.csv" -Encoding UTF8 -Append -NoTypeInformation -Delimiter ";"

With the above code, I think it took about an hour to complete the scan of ~2500 files (I went to lunch and didn’t measure it).
Notice the Start-Sleep cmdlet in the foreach loop to avoid being too aggressive with the virustotal public API.
Here are some samples of the screen output I had while processing files.

And here is the Excel file after 2 hours of work :-D

PowerShell rocks! 8-)

LogParser vs. PowerShell

The following article popped-up this morning about How to extract NETBIOS name from BADMIF files on ConfigMgr 2012 using Log Parser 2.2

Hey, why would I need the old school log parser from 2005 when we have PowerShell nowadays and then another tool to manipulate the data output?

You can actually achieve this task with a PowerShell one-liner and even without having to manipulate the output.

Get-ChildItem -Path "C:\Program Files\Microsoft Configuration Manager\inboxes\auth\\BADMIFS" -Include *.MIF -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { 
    try {
            Get-Content -ReadCount 1 -TotalCount 6 -Path $_.FullName -ErrorAction Stop  | 
            Select-String -Pattern "//KeyAttribute<NetBIOS\sName><(?<ComputerName>.*)>" -ErrorAction Stop 
    } catch {
        Write-Warning -Message "Failed" 

If you’ve got warnings, you may also want to investigate which files caused it.
In this case, I propose the following modification of the above code to be able to catch these files.

$ConfigMgrBoxPath = "C:\Program Files\Microsoft Configuration Manager\inboxes\auth\\BADMIFS"
Get-ChildItem -Path $ConfigMgrBoxPath -Include *.MIF -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { 
    $File = $_.FullName ;
    try {
            Get-Content -ReadCount 1 -TotalCount 6 -Path $_.FullName -ErrorAction Stop  | 
            Select-String -Pattern "//KeyAttribute<NetBIOS\sName><(?<ComputerName>.*)>" -ErrorAction Stop 
    } catch {
        Write-Warning -Message "Failed for $File" 

PowerShell rocks, no doubt! 8-)

Friday fun: Find all local groups and their members

Without my favourite tool (PowerShell) in a pure DOS command prompt, this is how I’d have displayed the groups and their members:

for /f "tokens=1,* delims=*" %i in ('net localgroup ^| findstr /R /C:"^\*" ') do @ (echo.&echo Group:%i&echo. & for /f "tokens=* skip=6" %z in ( 'net localgroup "%i" ^| findstr /v /C:"The command completed successfully."') do @ echo %z)

When mixing the native DOS command and some PowerShell regular expression, this is how I’d get the same result displayed:

(net localgroup) -match "^\*" -replace "\*","" | foreach { 
    "`nGroup:$($_)`n" ; 
    (net localgroup "$_") | Select-String -Pattern "^\-","^The\sCommand","^Alias\sname","^Comment","^Members","^(\S)?$" -notmatch

If I had to achieve the same task only in PowerShell without calling native DOS commands, I’d do:

([ADSI]"WinNT://$($env:computername),computer").psbase.children | where { $_.psbase.schemaClassName -eq 'group' } | foreach {
    ([ADSI]$_.psbase.Path).psbase.Invoke("Members") | foreach {
        $_.GetType().InvokeMember("Name",'GetProperty',$null,$_, $null)

Note that I’ve adapted the above code from PowerShell MVP Shay Levy that can be found on this page

I’ve modified his code because the following syntax (the children enumeration actually) falls in an infinite loop on my windows 8.1

$computer = [ADSI]"WinNT://$server,computer"

There are other annoyances. If I launch the following as a standard user it works


…but I get the following errors at the end of the enumeration:
format-default : The following exception occurred while retrieving members: “Unknown error (0×80005004)”
+ CategoryInfo : NotSpecified: (:) [format-default], ExtendedTypeSystemException
+ FullyQualifiedErrorId : CatchFromBaseGetMembers,Microsoft.PowerShell.Commands.FormatDefaultCommand

…and I don’t get the above error with Powershell running as Admin.

I wonder why all WinNT ADsPath don’t work in with the [ADSI], alias the System.DirectoryServices.DirectoryEntry .Net class.

Is my computer connected to the Internet

When I wrote my second (draft) article entitled How to view and restore hidden Windows Updates with PowerShell on I included the following code in a function to test whether the computer was connected to the Internet before initiating a scan.

    Function Test-ConnectedToInternet {

It raised some legitimate questions and of course I learned new things I’d like to share :-)

The first question was about how the Windows Update Agent is configured. In some locked-down environments, computers/servers might not be connected directly to Internet and the WUA is configured to use an internal WSUS server. The function I proposed works with WSUS but would have failed because of the above test I planned to initially add. It was actually wiser not to test whether the computer is connected to the Internet. In other words, a scan can be performed against WSUS even if the computer hasn’t access to the Internet.

The second question was about the ‘IsConnectedToInternet’ property.
The property I’m testing above is the one attached directly to the Network List Manager object.

The same property also exists for all the network interfaces present on the computer.

$nlm = [Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]"{DCB00C01-570F-4A9B-8D69-199FDBA5723B}"))
$nlm.GetNetworkConnections() | ForEach-Object { 
        NetworkName = ($_.GetNetwork().GetName());
        isConnectedToInternet = $_.isConnectedToInternet;
} | Format-Table -AutoSize 

So the property directly attached to the Network List Manager ( $nlm.isConnectedToInternet ) is somehow a global property set to summarize what was found per interface. If you’ve multiple interfaces and at least one has access to the Internet, then your computer has access to Internet.

On Windows 8.1, I’ve also got a new cmdlet function Get-NetConnectionProfile that will do the job.

Get-NetConnectionProfile -IPv4Connectivity Internet

Behind the scene this new cmdlet function queries and presents more nicely what you can find in the WMI repository by executing

Get-WmiObject -Namespace root/StandardCimv2 -Class MSFT_NetConnectionProfile

What is exactly the IPv4connectivity property returned by the Get-NetConnectionProfile cmdlet?
It’s easy to get its definition like this,

Get-NetConnectionProfile | Select -First 1 |            
Get-Member | Where Name -eq "IPv4Connectivity" |            
Select -ExpandProperty Definition
System.Object IPv4Connectivity {get=[Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetConnectionProfile.IPv4Connectivity]($this.PSBase.CimInstanceProperties['IPv4Connectivity'].Value);set=param($newValue)
          $this.PSBase.CimInstanceProperties['IPv4Connectivity'].Value = [System.UInt32][Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetConnectionProfile.IPv4Connectivity]$newValue;}

One can discover the translation being operated between numeric values and its literal definition like this:

0..4| % {            
'{0}>{1}'-f $_,            

Get the GUID list of installed programs

There was a thread this month on the mailing list where a member shared a list of GUIDs installed in his environment.

Later on, he also shared how he got it. I won’t blame him, I also used to be a WMIC guy before I discovered and learned Powershell.
Let’s just take a minute to see what (hassle) he went through:

I’m glad that so many people found the GUID list useful. Obtaining it didn’t take me too long, I used the following in a batch script and ran it on all of the machines on our network. The temp file keeps one machine from locking the master file while it queries. This is the script I used to grab it all:

@echo off
wmic product get identifyingnumber,name,vendor,version /FORMAT:csv | find "{" > "%temp%\%computername%-GUID.tmp"
type "%temp%\%computername%-GUID.tmp" >> "\\<SERVERNAME>\<SHARENAME>\GUID-MasterList.csv"
del "%temp%\%computername%-GUID.tmp"

I opened it in Excel, removed the first column (when exporting to CSV format it appends the hostname), saved it as a CSV (ignore the warning that you may lose some formatting if you don’t save it as a spreadsheet), and then used gVim -c “:sort u” GUID-MasterList.csv to remove redundant lines (or Notepad++ with TextFX plugin installed under TextFX Tools for a GUI method).

I tried experimenting with a way to strip the hostname from the file and sort it entirely command-line but I realized that I was starting to waste the time I just saved myself. That’s for another day. Along with fixing issues where a comma in the Vendor field will cause it to split in two values. I end up saving it as a spreadsheet anyway, so it’s only cosmetic.


Don’t say you’re too busy, you’ve wasted enough time. Just invest in teaching yourself Powershell and be confident that it will for sure save your time sooner or later.

Why WMIC isn’t a good idea?
Well the first line of code proposed, just don’t run on my computer :-(

To solve it I had to specify the fullpath of XSL file like this:

Then it doesn’t perform very fast.
We can compare the equivalent of the WMIC commandline

wmic --% product GET identifyingnumber,name,vendor,version /FORMAT:"C:\Windows\system32\wbem\en-US\csv.xsl"

to the WMI query performed in Powershell that will produce the same output

Get-WmiObject -Class win32_Product -Property identifyingnumber,name,vendor,version | ForEach-Object {
    '{0},{1},{2},{3},{4}' -f $env:COMPUTERNAME,$_.identifyingnumber,$_.Name,$_.Vendor,$_.Version

As you can see Powershell doesn’t perform that much faster.
In both cases, 10 seconds it’s far too much, don’t you think?

Now, let’s see the huge flexibility PowerShell delivers over WMIC by solving the following issues:

  1. Get rid of the computername in front of each line.
  2. Remove or replace the comma in the Vendor field
  3. Perform faster
  • Get rid of the computername in front of each line.
  • That’s easy as you don’t have this problem in Powershell:

    $properties = "identifyingnumber","name","vendor","version"            
    Get-WmiObject -Class win32_Product -Property $properties |             
    Select -Property $properties
  • Remove or replace the comma in the Vendor field
  • Idem in Powershell, you don’t have this problem. All you need to do to get a CSV format is to use the built-in cmdlet designed for this purpose and send the WMI objects selected through the pipeline like this:

    $properties = "identifyingnumber","name","vendor","version"            
    Get-WmiObject -Class win32_Product -Property $properties |             
    Select -Property $properties |             
    Export-Csv -Path C:\GUID.csv -Encoding Unicode -NoTypeInformation

    Then, in Excel, you do

    Select the file C:\Guid.csv,

    Select the delimiter ‘comma’ instead of default ‘tab’ and leave the text qualifier to “quote”

    As you can see in the following capture, I haven’t the problem and some vendors have a comma in their name.

  • Perform faster
  • Well 10 seconds to get the list of products is a huge amount of time. Let’s say that WMI isn’t the best approach as it performs too slowly.
    I can actually divide by 20 the execution time by directly gathering the information from the registry like this:

    $properties = "identifyingnumber","name","vendor","version"
    Get-ChildItem HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products | ForEach-Object {
        $root  = $_.PsPath
        $_.GetSubKeyNames() | ForEach-Object {
            try {
                $RegKeyPath = (Join-Path -Path (Join-Path -Path $root -ChildPath $_) -ChildPath InstallProperties)
                $obj = Get-ItemProperty -Path $RegKeyPath -ErrorAction Stop
                if ($obj.UninstallString) {
                        Path = $RegKeyPath;
                        Name = $obj.DisplayName ;
                        Vendor = $obj.Publisher ;
                        Version = $obj.DisplayVersion ; 
                        IdentifyingNumber = ($obj.UninstallString -replace "msiexec\.exe\s/[IX]{1}","")
            } catch {
    } | Select -Property $properties

    CQFD :-D