You are currently viewing How to inventory Chrome extensions

How to inventory Chrome extensions

  • Post category:SCCM
  • Post comments:1 Comment

With or without Google Chrome Enterprise, it is possible to inventory all Chrome extensions installed by users. This inventory is interesting if you want to manage Chrome efficiently, create blacklists and force the installation of certain extensions. It also allows you to highlight extensions that would be installed almost systematically by a service and to automate its installation.

The problem is that the extensions do not appear in the installed applications. They do have an entry in the registry but only the ID is filled in. Finally, it is necessary to bring up the extensions of all profiles.

How to inventory Chrome extensions? The script below clearly answers these problems. It allows you to inventory all the extensions by indicating its ID, the date of installation and on which profile it is present. The script records all this information in a new WMI class called “Chrome_Extensions”. It is then easy to add this class to the hardware inventory, as explained in this article. This script comes from Zach Sattler’s blog. Deploy it on your machines to pull up all the extensions:

<#
.SYNOPSIS
    Inventory Chrome Extension Information
.DESCRIPTION
    Evaluates every extension in every user profile and saves the information to a custom WMI namespace. Steps:
    1. Add all extension messages found to the $ExtensionMessage array
    2. Check for the WMI Namespace and create it if its missing
    3. Check for the Chrome_Extensions WMI Class and delete and recreate if it already exists
    4. Find all user profiles on the system and loop through them looking for extensions
    5. Check if the Chrome extension folder exists for that user profile
    6. Find all extension folders in the current profile and loop through them
    7. Check to see if there is more than 1 manifest.json file in the extension folder and get the newest dated one - This can happen if Chrome has downloaded an update for an extension but the update is still pending a restart of the browser
    8. Look inside the manifest.json file and save what's after '"version":' and what's after '"Name":'
    9. If the display name contains a ':' split it once and join everything after the first split - LastPass is a good example that has a ':' in the display name
    10. If '"Name":' is like '"__*"' then look for a language specific name under the _locales folder
        A. Look for an 'en' or 'en_US' folder. If both exist, use 'en'
        B. Inside the messages.json file loop through the $ExtensionMessage options looking for a match and save the '"message"' value
        C. Once the message value is populated save the value and break the loop
    11. Save the data into the Chrome_Extensions WMI Class
.EXAMPLE
    .\Set-ChromeExtensions.ps1
.NOTES
    Filename:   Set-ChromeExtensions.ps1
    Author:     Zach Sattler
    Contact:    @zsattler
    Created:    1/27/17
    Updated:    1/27/17
    Version:    1.0.0
    Requirements:
        - Define $Namespace and $Class variables to match your environment
        - Must be run as an Administrator
#>

# Ignore All Errors
$ErrorActionPreference = 'SilentlyContinue'

# WMI Variables
$Namespace = 'Chromespace'
$Class = 'Chrome_Extensions'

# Extension Message
$ExtensionMessage = @(
    'about_ext_name',
    'action_api',
    'app_name',
    'appFullName',
    'application_title',
    'appName',
    'app Name',
    'chrome_ext_short_name',
    'chrome_hangouts_short_name',
    'citrix_receiver',
    'DISPLAY_SERVICE_NAME',
    'extension_name',
    'extensionName',
    'ext_name',
    'extName',
    'ExtnName',
    'gaoptout_name',
    'gmailcheck_name',
    '4886126295094352182',
    '8969005060131950570',
    '"name":',
    'qs_name',
    'rss_subscription_name',
    'screenshotplugin_name',
    'NoteStationClipperSECTIONappKEYdisplayname',
    'themeName',
    'tv_name',
    'uwl_ext_chrome_name',
    'web2pdfExtnName',
    'web2pdfTitle',
    'webstore_pronghorn_product_name',
    'word_title'
)

# Function to create a Custom Namespace
Function CreateNamespace{
    $rootNamespace = [wmiclass]'root:__namespace'
    $NewNamespace = $rootNamespace.CreateInstance()
    $NewNamespace.Name = $Namespace
    $NewNamespace.Put() | Out-Null
}

# Function to create the Chrome_Extensions Class
Function CreateClass{
    $NewClass = New-Object System.Management.ManagementClass("root\$namespace", [string]::Empty, $null)
    $NewClass.name = $Class
    $NewClass.Qualifiers.Add("Static", $true)
    $NewClass.Properties.Add("Counter", [System.Management.CimType]::UInt32, $false)
    $NewClass.Qualifiers.Add("Description","Chrome_Extensions stores information on extensions add in Chrome.")
    $NewClass.Properties.Add("ProfilePath", [System.Management.CimType]::String, $false)
    $NewClass.Properties.Add("ManifestFolder", [System.Management.CimType]::String, $false)
    $NewClass.Properties.Add("FolderDate", [System.Management.CimType]::DateTime, $false)
    $NewClass.Properties.Add("Name", [System.Management.CimType]::String, $false)
    $NewClass.Properties.Add("Version", [System.Management.CimType]::String, $false)
    $NewClass.Properties.Add("ScriptLastRan", [System.Management.CimType]::String, $false)
    $NewClass.Properties["Counter"].Qualifiers.Add("Key", $true)
    $NewClass.Put() | Out-Null
}

# Function to get the extension name
Function GetDisplayName{
    param($ENPath,$Name)
    $indx = Select-String "$Name" $ENPath | ForEach-Object {$_.LineNumber}
    If($indx -ne $null){
        If(($indx).count -gt 1){$indx = $indx[0]}
        $indxNextLine = (Get-Content $ENPath)[$indx]
        # Citrix Receiver extension messages.json file contains 'app_name' and 'citrix_receiver'. Skip 'app_name' if the next line is like '*content*'
        If($indxNextLine -like '*content*'){}
        Else{
            If($indxNextLine -like '*description*'){
                $indx = $indx + 1
                $indxNextLine = (Get-Content $ENPath)[$indx]
            }
            # If the display name contains a ':' split it once and join everything after the first split
            $charCount = ($indxNextLine.ToCharArray() | Where-Object {$_ -eq ':'} | Measure-Object).Count
            If($charCount -gt 1){
                $label,$Value = $indxNextLine.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
                $Value = $Value -join ': '
            }
            Else{$label,$Value = $indxNextLine.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)}
            Return $label,$Value
        }
    }
    Else{}
}

# Check for WMI Namespace and create if missing
$NSfilter = "Name = '$Namespace'"
$NSExist = Get-WmiObject -Namespace root -Class __namespace -Filter $NSfilter
If($NSExist -eq $null){CreateNamespace}
# Check for WMI Class and recreate if it exists
$ClassExist = Get-CimClass -Namespace root/$Namespace -ClassName $Class -ErrorAction SilentlyContinue
If($ClassExist -eq $null){CreateClass}
Else{
    Remove-WmiObject -Namespace root/$Namespace -Class $Class
    CreateClass
}

# Counter variable
$j = 1
# Find User Profiles
$path = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*'
$ProfilePath = Get-ItemProperty -Path $path | Select-Object -Property ProfileImagePath
# Loop through each profile looking for extensions
$ProfilePath | ForEach-Object{
    # See if extension folder exists
    If(Test-Path ($_.profileimagepath + "\appdata\local\google\chrome\user data\default\extensions")){
        $ChromeProfileFolder = Get-ChildItem ($_.profileimagepath + "\appdata\local\google\chrome\user data\default\extensions") | ? { $_.PSIsContainer}
        # Loop through each extension folder
        ForEach ($CPF in $ChromeProfileFolder){
            # Check to see if there is more than 1 manifest.json file
            If((Get-ChildItem -Path $cpf.fullName -Filter manifest.json -Recurse).Count -gt 1){
                $NewestFolder = Get-ChildItem -Path $cpf.fullname | sort LastWriteTime | select -last 1
                $FolderDate = Get-ChildItem -Path $NewestFolder.FullName | Select-Object -last 1 | ForEach-Object {($_.lastwritetime.tostring("yyyyMMddhhmmss"))+ '.000000-000'}
                $UserPath,$Leftovers = $NewestFolder.FullName -split('appdata')
                $ManifestFile = (Get-ChildItem -Path $NewestFolder.FullName -filter manifest.json -Recurse).FullName
                $ManifestFileFolder = (Get-ChildItem -Path $NewestFolder.FullName -Filter manifest.json -Recurse).DirectoryName
            }
            Else{
                $FolderDate = Get-ChildItem $cpf.fullname | Select-Object -Last 1 | ForEach-Object {($_.lastwritetime.tostring("yyyyMMddhhmmss"))+ '.000000-000'}
                $UserPath,$Leftovers = $cpf.fullname -split('appdata')
                $ManifestFile = (Get-ChildItem -Path $cpf.fullName -Filter manifest.json -Recurse).FullName
                $ManifestFileFolder = (Get-ChildItem -Path $cpf.fullName -Filter manifest.json -Recurse).DirectoryName
            }
            # Get Version Number
            $VersionNumber = (Get-Content $ManifestFile | Where-Object {$_ -like '*"version":*'}) | foreach{$VersionLabel,$Version = $_.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)}
            # Get Extension Name
            $Name = (Get-Content $ManifestFile | Where-Object {$_ -like '*"name":*'})  | foreach{
                # If the display name contains a : split it once and join everything after the first split
                $charCount = ($_.ToCharArray() | Where-Object {$_ -eq ':'} | Measure-Object).Count
                If($charCount -gt 1){
                    $NameLabel,$NameValue = $_.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
                    $NameValue = $NameValue -join ': '
                    $NameToRecord = $NameValue
                }
                Else{
                    $NameLabel,$NameValue = $_.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
                    $NameToRecord = $NameValue
                }
                # No name in manifest.json. Search in messages.json
                If($NameValue -like "__*"){
                    Remove-Variable -Name EnglishMessages -Force
                    Remove-Variable -Name EnglishMessages2 -Force
                    Remove-Variable -Name ENPath -Force
                    $EnglishMessages = (Get-ChildItem -Path ($ManifestFileFolder + "\_locales\en") -filter messages.json -Recurse).FullName
                    $EnglishMessages2 = (Get-ChildItem -Path ($ManifestFileFolder + "\_locales\en_US") -filter messages.json -Recurse).FullName
                    # If en and en_US both exist use en
                    If($EnglishMessages -ne $null -and $EnglishMessages2 -ne $null){$EnglishMessages2 = $null}
                    If($EnglishMessages2 -ne $null){$ENPath = $EnglishMessages2}
                    Else{$ENPath = $EnglishMessages}
                    # Loop through each ExtensionName looking for a match
                    foreach($i in (0..((($ExtensionMessage).Count)-1))){
                        Remove-Variable -Name label -Force
                        Remove-Variable -Name NameToRecord -Force
                        Remove-Variable -Name output -Force
                        Remove-Variable -Name Value -Force
                        # Call GetDisplayName Function
                        $output = GetDisplayName $ENPath $ExtensionMessage[$i]
                        # Fill in $NameToRecord if $Output is not empty
                        If($output.count -eq 2){
                            $label = $output[0]
                            $Value = $output[1]
                            If($label -eq "message"){$NameToRecord = $Value}
                            Else{$NameToRecord = "Unknown"}
                            # Break out of the foreach loop
                            break
                        }
                    }
                }
                #  Save to WMI
                (Set-WmiInstance -Namespace root/$Namespace -Class $Class -Arguments @{
                Counter=$j;
                Name=$NameToRecord;
                ProfilePath=$Userpath;
                ManifestFolder=$ManifestFileFolder
                FolderDate=$FolderDate;
                Version=$Version;
                ScriptLastRan=Get-Date})
                $j=$j+1
            }
        }
    }
}

Once the WMI class has been added to the inventory, all Chrome extensions will come up.

This inventory is particularly useful when Chrome extensions are deployed by SCCM. The following article will explain how to deploy Chrome extensions.

This Post Has One Comment

Leave a Reply