none
SCCM Report: Google Chrome Extensions RRS feed

  • Question

  • I'd like to generate an SCCM report that shows all Google Chrome extensions installed on PCs similar to SCCM report for IE Browser Helper Object. Can someone please let me know how this is done in SCCM?

    Thanks,

    Gucci

    Tuesday, March 8, 2016 1:08 AM

All replies

  • First, you'd have to get ConfigMgr to collect the information in order to have data to generate a report from. For this, you can use software inventory to gather info about all *.crx files in c:\Users. From this, you can use a built-in report or write a custom one.

    Jason | http://blog.configmgrftw.com | @jasonsandys

    Tuesday, March 8, 2016 2:04 AM
  • Hi Jason,

    Thank you for your response.

    I did what you suggested but my report shows no data at all. After checking few machines, there's no file with crx extension even though they have Chrome installed with add-ins.

    Gucci

    Wednesday, March 9, 2016 2:40 AM
  • Ultimately, I have no knowledge of Google Chrome or Chrome Extensions (I'm a self-admitted Chrome hater) so my knowledge of what a Chrome extension is was based on some quick searches and thus may or may not be accurate. Thus, you may need to do some research to figure out exactly how to determine what Chrome extensions are installed on a system and based on that adjust the inventory method appropriately.

    One note though is that when I said c:\Users above, I meant c:\Users *and* all sub-folders. Not sure if that's what you configured or checked though.


    Jason | http://blog.configmgrftw.com | @jasonsandys

    • Proposed as answer by Frank DongModerator Thursday, March 24, 2016 9:43 AM
    • Unproposed as answer by Gucci100 Friday, March 25, 2016 6:45 PM
    Wednesday, March 9, 2016 4:05 PM
  • Yes, that's (c:\Users\*) what I did but doesn't work. There must be a way for SCCM to collect Chrome extensions installed on PCs managed by SCCM.

    Thanks,

    Gucci

    Friday, March 25, 2016 6:48 PM
  • C:\Users\* won't get you everything in subfolders of c:\users though. There's a check box to tell software inventory to check subfolders. Thus, for the pattern to search for, you would put *.crx, for location you would put c:\users, and you would check the search sub-folder option.

    Jason | http://blog.configmgrftw.com | @jasonsandys

    Friday, March 25, 2016 8:27 PM
  • I have the check-box for the sub-folders checked. The point that you're missing is that there's no file with .crx extension even though there're add-in with Chrome on the PCs, I've checked and verified this manually myself.
    Friday, March 25, 2016 11:24 PM
  • I have the check-box for the sub-folders checked. The point that you're missing is that there's no file with .crx extension even though there're add-in with Chrome on the PCs, I've checked and verified this manually myself.

    I would post this question in a Chrome forum, Ask them how you can tell that Chrome extension are installed on a computer from outside of Chrome. Someone there should have the answer. Once you know how to determine that Chrome extension are installed outside of Chrome then it is fairly easy for CM12 to inventory them.


    Garth Jones

    Blog: http://www.enhansoft.com/blog Old Blog: http://smsug.ca/blogs/garth_jones/default.aspx

    Twitter: @GarthMJ Book: System Center Configuration Manager Reporting Unleased

    Friday, March 25, 2016 11:54 PM
    Moderator
  • To use your phrase, the point you're missing is that this is a Microsoft Configuration Manager forum and I've already said that I don't know much about Chrome -- a non-Microsoft product -- and that you should do some research on your own on how to actually detect if a Chrome extension is installed. Once you figure that out, then we can help you configure ConfigMgr. Also, since you weren't being explicit abut exactly how you were configuring things, it's really hard to follow what you are doing -- you certainly did not in any way specify that you checked the check box. Getting technical help means being technically explicit and accurate others all we can do is guess.

    Jason | http://blog.configmgrftw.com | @jasonsandys

    Saturday, March 26, 2016 2:17 AM
  • So I found that in <each user profile>\AppData\Local\Google\Chrome\User Data\Default\Local Storage\, would be files ending in .localstorage, which happened to be in sqlite 3 format; and I originally thought "great, all the info will be in there".  but even after installing the powershell components needed (which in and of itself would be a logistical nightmare in production) so that the file would be query-able... the results were things like this: 
    yt-remote-connected-devices                            {123, 0, 34, 0...}                                   
    yt-remote-device-id                                    {123, 0, 34, 0...}                                   
    yt-remote-online-screens                               {123, 0, 34, 0...} 

    which didn't mean much.  I "knew", only because I had very few chrome extensions, that "meant"  Youtube (the yt).  But... pretty useless IMO for clear reports.

    so... next possibility.  The extensions themselves appear to be saved, per-user profile, in <each user profile>\AppData\Local\Google\Chrome\User Data\Default\Extensions\<subfolders>.  keeping in mind on this test box I have very few extensions actually installed, it looks like to my untrained eye that the file in each subfolder labeled "manifest.json" might be parsed to get *some* information, but depending upon the lack of information in "manifest.json", a subroutine within the script would be needed to go read ANOTHER subfolder file (per extension) under _locales, <language, likely en or en-US>, messages.json to get the english-y Name of the extension.  because as you know, Chrome is global, and it's possible that an extension might be created for multiple languages; so getting the english-y name only would be creative scripting.

    So, that's where the information is stored.  You'd need to create a script, my personal choice would be powershell, to do this...
    1) find where user profiles are stored on the system
    2) get each user profile folder; if there is a folder called \appdata\local\google\chrome\user data\default\extensions, continue in that profile.
       2a) for each subfolder in that ...\extensions folder, read manifest.json and look for two rows "name": " <some value>", and  "version": "<some value>"
       2b) if the result in "name:" = "__MSG_APP_NAME__" -- then that means the actual english-y name will be in a different file.
     2b1) go look for the english-y name in _locales\en\messages.json  (or _locales\en_US\messages.json if \en\messages.json doesn't exist) , for these two lines... when you find "app_name", you'll want the line after that's contents after "message":
         "app_name": {
          "message": "<The english-y name will be here>"
       2c) so for THAT one extension, now you'll have english-y name and version.  Of course, if you happen to be supporting only French language (or whatever), if you want to instead look for _locales\fr\message.json FIRST, and then if it doesn't exist look in \en or \en_US, that's up to you to create the script that way instead.
       2d) continue looping through each subfolder in the ...\extensions folder until you have all the names and versions.
    3) just like other script + mof edit routines, create and populate a custom wmi class that you'll make up completely with this custom data--I'd also include the <each user profile> folder in the results, so you know which user profile folder you found that stuff in.  I'd also... maybe... depending upon how easy or difficult it was to do, record two dates.  "scriptlastran" so you know how dated the info might be.  and a date for "FolderDate".  I noticed in testing that 1 folder in that \extensions seemed to get an updated date/time stamp when Chrome was launched.

    So that's where I *think maybe* the information is stored, and it appears to be vaguely possible to get some information out so that it's in your CM database; using a recurring script + mof edit.

    Note I have written no code, and have nothing to offer at this time.  If I have nothing better to do this evening maybe I'll try to get a beta script working.


    Standardize. Simplify. Automate.


    Monday, March 28, 2016 4:27 PM
  • This probably won't work and is a horrible idea.  But feel free to take the concept, try it out, and customize it until it works (if it ever will).

    http://mnscug.org/blogs/sherry-kissinger/440-inventory-google-chrome-extensions-with-configmgr


    Standardize. Simplify. Automate.

    Monday, March 28, 2016 10:29 PM
  • I know this is quite old, but I just came across Sherry's blog. It works pretty well, but I'm stuck on a few extensions that report a Name as "Unknown" even though I can find the correct name as 'appName' in the messages.json file. Any advice?
    Wednesday, January 25, 2017 8:50 PM
  • She got you 98% there. Since they are Json files, powershell can convert these. Which might work better than doing a For-Each loop.
    Tuesday, January 31, 2017 4:55 PM
  • I know this is quite old, but I just came across Sherry's blog. It works pretty well, but I'm stuck on a few extensions that report a Name as "Unknown" even though I can find the correct name as 'appName' in the messages.json file. Any advice?

    This is even older now but just ran across a need for this myself and ran into the same 'unknown' extension support, unfortunately for one that we really wanted to inventory. The issue is the assumption that the 'messages' value will always appear as the line after 'appName', 'app_name', or 'app Name' which isn't the case. I didn't get too fancy fixing this. In my test cases the 'messages' value was always the first or second line after the appName values so I tossed in a quick 'else' on all of the applicable if/thens, Example:

    if ($label3.trim() -eq "message") { $NameToRecord = $Value3 } else
            {
              $indx1NextLine = (Get-Content $EnglishMessages)[$indx+1]
              $Label3,$Value3 = $indxNextLine.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
              if ($label3.trim() -eq "message") { $NameToRecord = $Value3 }
            }

    Far from perfect but a quick fix that resolves my test cases adequately.



    • Edited by J LeMay Tuesday, August 1, 2017 12:20 PM
    • Proposed as answer by Sherry Kissinger Tuesday, August 1, 2017 2:15 PM
    Tuesday, August 1, 2017 12:14 PM
  • This is even older now, but still seems to be the most pertinent answer to the problem.  After putting in J LeMay's updates, I was still running into cases of Unknown because the JSON wasn't using any form of 'appName' for the name.  For instance, Google Play Music was using: "name": "__MSG_2714752802779336020__" .  I had a few other similar cases, with one saying "__MSG_rss_subscription_name__" and another with "__MSG_extName__".  

    I ended up adding another loop into the subfolder loop, which pulls out the unique data from those strings and uses it to find the correct Extension Name.  In my testing, it seems that this one check will work to replace all 3 of the original checks in each of the EnglishMessages sections in addition to finding oddly named messages as well.  I'm guessing I should be able to simplify the subfolder checks now, but haven't started working on it yet.

    if ($value2 -like "__*") {
    									   Remove-Variable -Name $EnglishMessages
    									   Remove-Variable -Name $EnglishMessages2
    									     $EnglishMessages = (gci -path ($ManifestFileFolder + "\_locales\en") -filter messages.json -Recurse).FullName 
    									     $EnglishMessages2 = (gci -path ($ManifestFileFolder + "\_locales\en_US") -filter messages.json -Recurse).FullName
    										 
    										#----------------------------------
    										#8.        Loop though the subfolders until first 1 is found at all, then read that message.json until you get app_name
    										#----------------------------------
    									   if (test-path $EnglishMessages) {
    #									      $label3 = ""
    #									      $label4 = ""
    #									      $label5 = ""
    										  $label6 = ""
    #									      $indxNextLine1 = ""
    #									      $indxNextLine2 = ""
    #									      $indxNextLine3 = ""
    										  $indxNextLine4 = ""
    #									      $indx1 = ""
    #									      $indx2 = ""
    #									      $indx3 = ""
    										  $indx4 = ""
    #										  $Value3 = ""
    #										  $Value4 = ""
    #										  $Value5 = ""
    										  $Value6 = ""
    										  $MessageNameToFind = $Value2.Trim("_").Trim("MSG_")
    
    #									      $indx1 = Select-String 'app_name' $EnglishMessages | ForEach-Object {$_.LineNumber}
    #									      	$indxNextLine1 = (Get-Content $EnglishMessages)[$indx1]
    #									      	$Label3,$Value3 = $indxNextLine1.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    #									      $indx2 = Select-String 'appName' $EnglishMessages | ForEach-Object {$_.LineNumber}
    #									      	$indxNextLine2 = (Get-Content $EnglishMessages)[$indx2]
    #									      	$Label4,$Value4 = $indxNextLine2.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    #									      $indx3 = Select-String 'app Name' $EnglishMessages | ForEach-Object {$_.LineNumber}
    #									      	$indxNextLine3 = (Get-Content $EnglishMessages)[$indx3]
    #									      	$Label5,$Value5 = $indxNextLine3.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    									      $indx4 = Select-String $MessageNameToFind $EnglishMessages | ForEach-Object {$_.LineNumber}
    									      	$indxNextLine4 = (Get-Content $EnglishMessages)[$indx4]
    									      	$Label6,$Value6 = $indxNextLine4.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    										  
    #										  if ($label3.trim() -eq "message") { $NameToRecord = $Value3 } else #{$NameToRecord = ""}
    #										  		{
    #										          $indxNextLine1 = (Get-Content $EnglishMessages)[$indx1+1]
    #										          $Label3,$Value3 = $indxNextLine1.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    #										          if ($label3.trim() -eq "message") { $NameToRecord = $Value3 }
    #										        }
    #									      if ($label4.Trim() -eq "message") { $NameToRecord = $Value4} else #{$NameToRecord = ""}
    #										  		{
    #										          $indxNextLine2 = (Get-Content $EnglishMessages)[$indx2+1]
    #										          $Label4,$Value4 = $indxNextLine2.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    #										          if ($label4.trim() -eq "message") { $NameToRecord = $Value4 }
    #										        }
    #									      if ($label5.Trim() -eq "message") { $NameToRecord = $Value5} else #{$NameToRecord = ""}
    #										  		{
    #										          $indxNextLine3 = (Get-Content $EnglishMessages)[$indx3+1]
    #										          $Label5,$Value5 = $indxNextLine3.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    #										          if ($label5.trim() -eq "message") { $NameToRecord = $Value5 }
    #										        }
    										  if ($label6.Trim() -eq "message") { $NameToRecord = $Value6} else #{$NameToRecord = ""}
    										  		{
    										          $indxNextLine4 = (Get-Content $EnglishMessages)[$indx4+1]
    										          $Label6,$Value6 = $indxNextLine4.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    										          if ($label6.trim() -eq "message") { $NameToRecord = $Value6 }
    										        }
    									      if ($NameToRecord -eq "") { $NameToRecord = "Unknown"}
    									       }  #EnglishMessages
    
    									   if (($NameToRecord -like '__*') -and (test-path $EnglishMessages2)) {
    #									      $label3 = ""
    #									      $label4 = ""
    #									      $label5 = ""
    										  $label6 = ""
    #									      $indxNextLine1 = ""
    #									      $indxNextLine2 = ""
    #									      $indxNextLine3 = ""
    										  $indxNextLine4 = ""
    #									      $indx1 = ""
    #									      $indx2 = ""
    #									      $indx3 = ""
    										  $indx4 = ""
    #										  $Value3 = ""
    #										  $Value4 = ""
    #										  $Value5 = ""
    										  $Value6 = ""
    										  $MessageNameToFind = $Value2.Trim("_").Trim("MSG_")
    
    #									      $indx1 = Select-String 'app_name' $EnglishMessages2 | ForEach-Object {$_.LineNumber}
    #									      	$indxNextLine1 = (Get-Content $EnglishMessages2)[$indx1]
    #									      	$Label3,$Value3 = $indxNextLine1.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    #									      $indx2 = Select-String 'appName' $EnglishMessages2 | ForEach-Object {$_.LineNumber}
    #									      	$indxNextLine2 = (Get-Content $EnglishMessages2)[$indx2]
    #									      	$Label4,$Value4 = $indxNextLine2.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    #									      $indx3 = Select-String 'app Name' $EnglishMessages2 | ForEach-Object {$_.LineNumber}
    #									      	$indxNextLine3 = (Get-Content $EnglishMessages2)[$indx3]
    #									      	$Label5,$Value5 = $indxNextLine3.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    										  $indx4 = Select-String $MessageNameToFind $EnglishMessages2 | ForEach-Object {$_.LineNumber}
    									      	$indxNextLine4 = (Get-Content $EnglishMessages2)[$indx4]
    									      	$Label6,$Value6 = $indxNextLine4.split(':').trim().trim([Char]0x002C).trim([Char]0x0022)
    										  										
    #									      if ($label3.trim() -eq "message") { $NameToRecord = $Value3 } else #{$NameToRecord = ""}
    #										  		{
    #										          $indxNextLine1 = (Get-Content $EnglishMessages2)[$indx1+1]
    #										          $Label3,$Value3 = $indxNextLine1.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    #										          if ($label3.trim() -eq "message") { $NameToRecord = $Value3 }
    #										        }
    #									      if ($label4.Trim() -eq "message") { $NameToRecord = $Value4} else #{$NameToRecord = ""}
    #										  		{
    #										          $indxNextLine2 = (Get-Content $EnglishMessages2)[$indx2+1]
    #										          $Label4,$Value4 = $indxNextLine2.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    #										          if ($label4.trim() -eq "message") { $NameToRecord = $Value4 }
    #										        }
    #									      if ($label5.Trim() -eq "message") { $NameToRecord = $Value5} else #{$NameToRecord = ""}
    #										  		{
    #										          $indxNextLine3 = (Get-Content $EnglishMessages2)[$indx3+1]
    #										          $Label5,$Value5 = $indxNextLine3.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    #										          if ($label5.trim() -eq "message") { $NameToRecord = $Value5 }
    #										        }
    										  if ($label6.Trim() -eq "message") { $NameToRecord = $Value6} else #{$NameToRecord = ""}
    										  		{
    										          $indxNextLine4 = (Get-Content $EnglishMessages2)[$indx4+1]
    										          $Label6,$Value6 = $indxNextLine4.split(':').trim().trim([Char]0x002c).trim([Char]0x0022)
    										          if ($label6.trim() -eq "message") { $NameToRecord = $Value6 }
    										        }
    									      if ($NameToRecord -eq "") { $NameToRecord = "Unknown"}
    									       }  #EnglishMessages2
    
    									    } #__something for a name 

    Thursday, March 7, 2019 2:50 PM
  • One more update and, again, sorry for bringing up an old post (though it's not as old since I just brought it up a couple of weeks ago). :)   

    My requirements were to inventory Chrome, Mozilla, and Edge extensions.  Sherry's script got me going in the right direction and J Lemay's updates made it even better, but I ended up making heavy modifications to make it more efficient, use Functions, and add the ability to inventory Mozilla and Edge as well.  One note with Edge, the extensions come up as Windows Apps (Store Apps) and I didn't find a way to isolate just the Edge extensions versus any standard Windows Apps so, if you make heavy use of Store Apps, you'll likely see some pretty big lists.  Anyway, here is the final script in full. 

    Note that I changed the WMI class to be more generic and added a couple of extra fields.  I didn't find a way to remove the WMI class via script and you don't seem to be able to add fields to it so, if you want to continue using the ChromeExtensions class on a machine you've already created it on, you'll need to manually add the fields or delete the class, both using wbemtest.  Also, this limitation is why I've added a v1 to the end of the WMI class, in case I need to recreate it later, I'll name it v2.

    Finally, I added a section (I think I added it, I don't remember if it was in Sherry's original script) to kick off the ConfigMgr hardware inventory using a Delta update.  The inventory only happens if the script find a non-excluded Extension in one of the 3 browsers but, if you run this wide scale (I am kicking it off using a GPP pushed Scheduled Task), you may inundate your ConfigMgr Site Server for a while during processing.  If you don't want to use this automatic update, simply comment out the 'StartConfigMgrInventory $false' line at the very bottom of the script and ConfigMgr will pick it up during the next schedule Hardware Inventory.

    # The script queries the selected profile paths to find installed browser extensions and write them into a custom WMI class to be picked up by ConfigMgr Hardware Inventory
    
    Start-Transcript -Path "$env:SystemRoot\Temp\BrowserInventory.log"
    
    ################################################################################
    # ***** Configuration Options *****
    ################################################################################
    $ErrorActionPreference = "SilentlyContinue" # Override the default to hide errors.  Comment this line to show standard error messgaes.
    
    $boolInventoryChrome = $true
    $boolInventoryMozilla = $true
    $boolInventoryEdge = $true
    
    $strChromeProfilePathAppend = "AppData\Local\Google\Chrome\User Data\Default\Extensions"
    $strMozillaProfilePathAppend = "AppData\Roaming\Mozilla\FireFox\Profiles"
    $strEdgeProfilePath = "$env:ProgramFiles\WindowsApps"
    $CustomWMIClassName = "cm_BrowserExtensionsV1"
    
    $boolExcludeCommonExtensions = $true  #optional, if you want to exclude the following common browser extensions (these are typically preinstalled or mass installed)
    $strCommonGoogleExtensions = 'Google Docs','Google Sheets','Google Slides','Google Drive','YouTube','Gmail','Google Docs Offline','Chrome Web Store Payments','Chrome Media Router'
    $strCommonMozillaExtensions = ''
    $strCommonEdgeExtensions = ''
    $strCommon3rdPartyExtensions = 'Adobe Acrobat'
    
    $boolInventoryPreinstalledMicrosoftApps = $false # there are hundreds of preinstalled Apps from Microsoft.  If you want to inventory them, set this to $true, otherwise they will be skipped
    
    #Combine the excluded extension lists for use later
    $strCommonExtensions = $strCommonGoogleExtensions + $strCommonMozillaExtensions + $strCommonEdgeExtensions + $strCommon3rdPartyExtensions
    
    
    
    ################################################################################
    # ***** THE MAIN SCRIPT STARTS HERE - NO MODIFICATION SHOULD BE NECESSARY *****
    ################################################################################
    
    
    # ***** Functions *****
    
    
    # Function to create the custom WMI class
    # Note that, once the class has been created on a device, it must be manually deleted if you want to add any additional columns of data
    Function Prepare-Wmi-Class()
    	{
    		Get-WMIObject $CustomWMIClassName -ErrorAction SilentlyContinue -ErrorVariable strWMIClassError | Out-Null
    		
    		# If the GET failed, the class doesn't exist, so create it.  If not, it exists so clean it out.
    		If ($strWMIClassError)
    			{
    				Write-Host "WMI Class $CustomWMIClassName does not exist.  Try to create it.`n" -ForegroundColor Green
    				
    				Try
    					{
    						$newClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null); 
    
    						$newClass["__CLASS"] = "$CustomWMIClassName";
    
    						$newClass.Qualifiers.Add("Static", $true)
    						$newClass.Properties.add("Counter", [System.Management.CimType]::UInt32, $false)
    						$newClass.Properties.add("ProfilePath", [System.Management.CimType]::String, $false)
    						$newClass.Properties.add("FolderDate", [System.Management.CimType]::DateTime, $false)
    						$newClass.Properties.add("FolderName", [System.Management.CimType]::String, $false)
    						$newClass.Properties.add("Browser", [System.Management.CimType]::String, $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]::DateTime, $false)
    						$newClass.Properties["Counter"].Qualifiers.Add("Key", $true)
    						$newClass.Put()
    					}
    					Catch
    						{
    							Write-Host "Could not create WMI class" -ForegroundColor Red
            					Exit 1
    						}
    			}
    			Else
    				{
    					Write-Host "WMI Class $CustomWMIClassName exists.  Clear it out and proceed with logging.`n" -ForegroundColor Green
    					
    					# Remove all existing instances of the WMI class
    					Get-WmiObject $CustomWMIClassName | Remove-WmiObject
    				}
    	}
    
    
    # Extract the Mozilla files
    Function ExtractMozillaExtensionPackage ($strMozillaExtensionPackagePath, $strBrowserDataToFind)
    	{
    		# If the temp extraction directory already exists, remove it	
    		$strTempFolderRoot = "$strMozillaExtensionPackagePath\SLPS_TEMP"
    		If (Test-Path $strTempFolderRoot) {Remove-Item $strTempFolderRoot -Recurse -Force}
    		
    		# Get the list of XPI files
    		$arrExtensionPackages = (Get-ChildItem -Path $strMozillaExtensionPackagePath -Filter "*.xpi" -Recurse).FullName
    		
    		# Bind to each XPI file and unzip it
    		ForEach ($objExtensionPackage in $arrExtensionPackages)
    			{
    				$objArchiveFile = Get-ChildItem -Path $objExtensionPackage
    				$objArchiveName = $objArchiveFile.Name
    
    				# Open the XPI file
    				$objArchive = [System.IO.Compression.ZipFile]::OpenRead($objArchiveFile)
    		
    				# Try to unzip it
    				Try
    					{
    						# Define the target of the extracted files
    						$strTempFolder = "$strTempFolderRoot\$objArchiveName".Trim(".xpi") + "\Extracted"
    					
    						# If the temp folder doesn't exist, create it
    						If (!(Test-Path $strTempFolder)) {New-Item -ItemType "Directory" $strTempFolder | Out-Null}
    						
    						# Extract the files
    						[System.IO.Compression.ZipFileExtensions]::ExtractToDirectory($objArchive, $strTempFolder)
    						
    					}
    					Catch
    						{
    							Write-Host "Error extracting $objArchiveName $_" -ForegroundColor Red
    						}
    				
    			}
    		
    		# Read the manifest files
    		GetManifestFiles $strTempFolderRoot $strBrowserDataToFind "manifest.json"
    		
    		# Remove the temp directory when complete - this isn't really necessary, as it gets removed the next time the script runs.  It's also helpful for troubleshooting to leave it.
    #		Remove-Item $strTempFolderRoot -Recurse -Force
    
    	}
    
    
    # Inventory the browser extensions
    Function InventoryExtensions  ($strUserProfilePath, $strBrowserDataToFind)
    	{
    		# Browser specific searches
    		
    		# For Chrome, just search the given path
    		If ($strBrowserDataToFind -eq "CHROME") 
    			{
    				# Append the Chrome path
    				$strManifestSearchPath = $strUserProfilePath + "\" + $strChromeProfilePathAppend
    				
    				# Read the manifest files
    				GetManifestFiles $strManifestSearchPath $strBrowserDataToFind "manifest.json"
    			}
    		
    		# If it's Mozilla, we have to extract the packages into a temp folder first
    		If ($strBrowserDataToFind -eq "MOZILLA") 
    			{
    				If (Test-Path "$strUserProfilePath\$strMozillaProfilePathAppend") 
    					{
    						# Set the path to the profiles.
    						$arrMozillaProfiles = Get-ChildItem "$strUserProfilePath\$strMozillaProfilePathAppend" | Where-Object { $_.PSIsContainer}
    						
    						# For each profile folder, we need to extract the XPI files (zipped extension pacakges) into a temp folder, then run the Manifest check
    						ForEach ($objMozillaProfile in $arrMozillaProfiles)
    							{
    								# Run the Extraction routine against the Extension profile folder
    								ExtractMozillaExtensionPackage "$($objMozillaProfile.FullName)\Extensions" $strBrowserDataToFind
    							}
    					}
    			}
    			
    		# For Edge, we'll use PowerShell cmdlets to get the data
    		If ($strBrowserDataToFind -eq "EDGE") 
    			{
    				# Set the Edge path
    				$strManifestSearchPath = $strEdgeProfilePath
    				
    				# Get the username from the profile path
    				$strAppxUser = $strUserProfilePath.Replace("$strProfileRoot\","")
    				
    				# Get a list of Apps for this user
    				$objUserAppxPackages = Get-AppxPackage -User $strAppxUser
    				
    				# For each app, run the GetManifest function
    				ForEach ($objUserAppxPackage in $objUserAppxPackages)
    					{
    						# If flag is not set to $true and the App Publisher contains "Microsoft", don't inventory it. Otherwise, proceed with the inventory.
    						If ($boolInventoryPreinstalledMicrosoftApps -eq $true -or ($boolExcludePreinstalledMicrosoftEdgeExtensions -ne $true -and $objUserAppxPackage.Publisher -notlike "*Microsoft*"))
    							{
    								# Clear the variables
    								$global:dtFolderDateToRecord = Get-Date
    								$global:strExtensionFolderNameToRecord = ""
    								
    								# Set the FolderName to the installation folder
    								$global:strExtensionFolderNameToRecord = ($objUserAppxPackage.InstallLocation).Replace("$strEdgeProfilePath\","")
    							
    								# Try to get the date from the installation folder
    								Try
    									{
    										$global:dtFolderDateToRecord = Get-ChildItem $objUserAppxPackage.InstallLocation | Select-Object -Last 1 | ForEach-Object { ($_.lastwritetime.tostring("yyyyMMddhhmmss"))+ '.000000-000'  } 
    									}
    									Catch 
    										{
    											Write-Host "Could not record the date for $($objUserAppxPackage.InstallLocation): $_"
    											$global:dtFolderDateToRecord = "19000101000000" + '.000000-000'
    										}
    							
    								# Read the manifest files
    								ReadAppxManifestXML $objUserAppxPackage.PackageFullName $strAppxUser $strBrowserDataToFind
    							}
    							#Else {Write-Host "$($objUserAppxPackage.PackageFullName) - Skipped"}
    					}
    			}
    	}
    
    			
    # Get a list of all manifest files in the given path
    Function GetManifestFiles ($strManifestSearchPath, $strBrowserDataToFind, $strDefaultManifestFileName)
    	{
    		# Clear the variables
    		$global:dtFolderDateToRecord = ""
    		$global:strExtensionFolderNameToRecord = ""
    
    		# Search the path
    		If (Test-Path $strManifestSearchPath)
    			{
    				Try
    					{
    						# Get an array of Extension folders from the Manifest File Search Path - this should be a list of folders, each containing the Extension files
    						$arrExtensionFolders = Get-ChildItem $strManifestSearchPath | Where-Object { $_.PSIsContainer}
    							
    						# Go through the array of Extension folders
    						ForEach ($objExtensionFolder in $arrExtensionFolders) 
    							{
    								# If this is Chrome, there will be a list of Version folders under the Extension folder so we need to iterate those.  For other browsers, we'll fake it.
    								$arrExtensionVersionFolders = Get-ChildItem $objExtensionFolder.FullName | Where-Object { $_.PSIsContainer}
    			
    								# Go through the version folders in the extension folder
    								ForEach ($objExtensionVersionFolder in $arrExtensionVersionFolders)
    									{
    										# Record the Extension Folder Name
    										$global:strExtensionFolderNameToRecord = $objExtensionFolder.Name
    							
    										# Record the Extension Folder Date
    										$global:dtFolderDateToRecord = Get-ChildItem $objExtensionVersionFolder.FullName | Select-Object -Last 1 | ForEach-Object { ($_.lastwritetime.tostring("yyyyMMddhhmmss"))+ '.000000-000'  } 
    																
    										# Inside each version folder, get the manifest.json file
    										$strManifestFilePath = (Get-ChildItem -Path $objExtensionVersionFolder.FullName -filter $strDefaultManifestFileName -Recurse -ErrorAction SilentlyContinue).FullName
    									
    										# If the manifest file exists, read it
    										If ($strManifestFilePath)
    											{
    												# If the Manifest file is manifest.json, call that function to find the Extension Name and Version
    												If ($strDefaultManifestFileName = "manifest.json")
    													{
    														ReadManifestJSONFile $strManifestFilePath
    													}
    												
    												# Record the info to WMI
    												RecordExtensionsToWMI $strExtensionNameToRecord $strUserProfilePath $dtFolderDateToRecord $strExtensionFolderNameToRecord $strBrowserDataToFind $strVersionToRecord
    											}
    									}
    							}
    					}
    					Catch
    						{Write-Host "Error reading manifest files $_" -ForegroundColor Red}
    			}
    	}
    
    
    # Read the required info from the manifest.json file
    Function ReadManifestJSONFile ($strManifestFilePath)
    	{
    		# Clear the variables
    		$strVersionLabelFromManifest = ""
    		$strNameLabelFromManifest = ""
    		$strVersionValueFromManifest = ""
    		$strNameValueFromManifest = ""
    		$global:strVersionToRecord = ""
    		$global:strExtensionNameToRecord = ""		
    	
    		If (Test-Path $strManifestFilePath) 
    			{
    			
    				ForEach ($objManifestFilePath in $strManifestFilePath)
    					{
    						# Read the Manifest file into an object
    						$objManifestJSONFile = Get-Content $strManifestFilePath | ConvertFrom-Json
    					
    						# Inside manifest.json file, read the Version info
    						$strVersionValueFromManifest = $objManifestJSONFile.Version
    	
    						# Record the version info
    						$global:strVersionToRecord = $strVersionValueFromManifest
    							
    						# Inside manifest.json file, read the Name info
    						$strNameValueFromManifest = $objManifestJSONFile.Name
    											   
    						# If Name starts with underscores (i.e. "__MSG_APP_NAME__") then look in the messages.json files for a language specific name.  Otherwise, use the name from the routine above.
    					   	If ($strNameValueFromManifest -like "__*") 
    							{
    								# Get the name to search for  in messages.json
    								$strMessageNameToFind = $strNameValueFromManifest.Trim("_").Trim("MSG_")
    								
    							   	# If the variables already exist, remove them
    								If ($strMessagesFile1) {Remove-Variable -Name strMessagesFile1}
    								If ($strMessagesFile2) {Remove-Variable -Name strMessagesFile2}
    						
    								# Set the Messages folder variables to the 2 likely English language folders
    								$objManifestFileFolder = (Get-ChildItem $strManifestFilePath).DirectoryName
    						
    								$strMessagesFile1 = "$objManifestFileFolder\_locales\en\messages.json"
    							    $strMessagesFile2 = "$objManifestFileFolder\_locales\en_US\messages.json"
    
    								# Call the function to search the messages.json file
    								If (Test-Path $strMessagesFile1)
    									{
    										ReadMessagesFile $strMessagesFile1 $strMessageNameToFind
    									}
    
    								# If the name wasn't found in the first file, search the second
    								If ($strExtensionNameToRecord -eq "" -and (Test-Path $strMessagesFile2))
    									{
    										# Call the function with the second file
    										ReadMessagesFile $strMessagesFile2 $strMessageNameToFind
    									}
    						
    								# If the name wasn't found in the second file, record it as Unknown
    								If ($strExtensionNameToRecord -eq "") {$strExtensionNameToRecord = "Unknown"}
    								
    							}
    							Else
    								{$global:strExtensionNameToRecord = $strNameValueFromManifest}
    					}
    				
    			}
    	}
    
    	
    
    # Read the required info from the AppxManifest.XML
    Function ReadAppxManifestXML ($strAppxManifestPackageName, $strAppxUser, $strBrowserDataToFind)
    	{
    		# Clear the variables
    		$global:strExtensionNameToRecord = ""		
    		$global:strVersionToRecord = ""
    	
    		# Record the package name
    		$global:strExtensionNameToRecord = (Get-AppxPackageManifest -Package $strAppxManifestPackageName -User $strAppxUser).Package.Properties.DisplayName
    
    		#Write-Host "Name: $strExtensionNameToRecord"
    		
    		# Record the package version
    		$global:strVersionToRecord = (Get-AppxPackageManifest -Package $strAppxManifestPackageName -User $strAppxUser).Package.Identity.Version
    
    		#Write-Host "Version: $strVersionToRecord"
    		
    		# Record the info to WMI
    		RecordExtensionsToWMI $strExtensionNameToRecord $strUserProfilePath $dtFolderDateToRecord $strExtensionFolderNameToRecord $strBrowserDataToFind $strVersionToRecord
    	}	
    	
    	
    	
    # Read the required info from the messages.json file
    Function ReadMessagesFile ($strMessagesFilePath, $strMessageNameToFind)
    	{
    		# Clear the variable
    		$global:strExtensionNameToRecord = ""
    	
    		# Search the given folder to find a messages.json file
    		If (Test-Path $strMessagesFilePath) 
    			{
    				# Clear the variables
    				$strNameLabelFromMessages = ""
    				$strNameValueFromMessages = ""
    				$intMessagesLineNumber = ""
    				$intMessagesNextLineNumber = ""
    								
    				# Search the messages.json file to find the specified text and get the line number
    		      	$intMessagesLineNumber = Select-String """$strMessageNameToFind""" $strMessagesFilePath | ForEach-Object {$_.LineNumber}
    				
    				# Get the content of the next line as that one should have the name
    		      	$intMessagesNextLineNumber = (Get-Content $strMessagesFilePath)[$intMessagesLineNumber]
    				
    				# Trim the Value of any spaces, commas, and double-quotes   #Value 6 should be the name
    		      	$strNameLabelFromMessages,$strNameValueFromMessages = $intMessagesNextLineNumber.Split(':').Trim().Trim([Char]0x002c).Trim([Char]0x0022)
    		  
    		  		# Check to see if Label6 was actually the 'Message' that we were looking for.  Sometimes it's the next (or the next) line.
    		  		If ($strNameLabelFromMessages.Trim() -eq "message") {$global:strExtensionNameToRecord = $strNameValueFromMessages} 
    					Else
    		  				{
    							# If it wasn't right, go to the next line, get the content, and trim it.
    				          	$intMessagesNextLineNumber = (Get-Content $strMessagesFilePath)[$intMessagesLineNumber+1]
    				          	$strNameLabelFromMessages,$strNameValueFromMessages = $intMessagesNextLineNumber.Split(':').Trim().Trim([Char]0x002c).Trim([Char]0x0022)
    				          
    					  		# Check it again
    					  		If ($strNameLabelFromMessages.Trim() -eq "message") {$global:strExtensionNameToRecord = $strNameValueFromMessages} 
    								Else
    								  	{
    										# If it still isn't right, check one more time.  Get the content and trim it.
    										$intMessagesNextLineNumber = (Get-Content $strMessagesFilePath)[$intMessagesLineNumber+2]
    						          		$strNameLabelFromMessages,$strNameValueFromMessages = $intMessagesNextLineNumber.Split(':').Trim().Trim([Char]0x002c).Trim([Char]0x0022)
    				
    										# If it found something that time, record it
    										If ($strNameLabelFromMessages.Trim() -eq "message") {$global:strExtensionNameToRecord = $strNameValueFromMessages }
    									}
    				        }
    	      
    		  		# If none of those checks found the right name, record the name as Unknown
    		  		If ($strExtensionNameToRecord -eq "") {$global:strExtensionNameToRecord = "Unknown"}
    	       } 
    	}
    	
    	
    # Write the specified info into WMI
    Function RecordExtensionsToWMI ($strNameWMI, $strProfilePathWMI, $dtFolderDateWMI, $strFolderNameWMI, $strBrowserWMI, $strVersionWMI)
    	{
    		# Output the Name and Version
    		Write-Host -NoNewline "$strBrowserWMI / $strNameWMI / $strVersionWMI"
    		#Write-Host -NoNewLine " / $strProfilePathWMI / $dtFolderDateWMI / $strFolderNameWMI / $intBrowserExtensionCount" # Additional logging data
    	
    		# Check to see if the Extension name is in the list of Extensions to Include
    		If ($boolExcludeCommonExtensions -eq $true) 
    			{
    		    	If ($strCommonExtensions -match $strExtensionNameToRecord) 
    					{
    				    	# The extension name is on the list so skip it and output
    						Write-Host " - Excluded" -ForegroundColor Red
    						
    						Return
    					}
    		    }
    		    
    			# Record into WMI
    		    (Set-WmiInstance -Path \\.\root\cimv2:$CustomWMIClassName  -Arguments @{
    		        Counter=$intBrowserExtensionCount;
    		        Name=$strNameWMI;
    		        ProfilePath=$strProfilePathWMI;
    		        FolderDate=$dtFolderDateWMI;
    				FolderName=$strFolderNameWMI;
    				Browser=$strBrowserWMI;
    		        Version=$strVersionWMI;
    		        ScriptLastRan=$dtScriptRunTime}) | Out-Null
    		          
    				# Increment the counter  
    				$global:intBrowserExtensionCount++
    		         
    				Write-Host " - Recorded" -ForegroundColor Green
    	         
    	}
    
    
    # Start a ConfigMgr hardware scan
    Function StartConfigMgrInventory ($boolRunFullInventory)
    	{
    		# Set the type of scan to ConfigMgr Hardware Inventory
    		$strScanType = "{00000000-0000-0000-0000-000000000001}"
    
    		# If specified, wipe out the current Hardware Inventory to initiate a full refresh.
    		If ($boolRunFullInventory -eq $true)
    			{
    				Get-WmiObject -Namespace "root\ccm\invagt" -Class InventoryActionStatus | where {$_.InventoryActionID -eq "$strScanType"} | Remove-WmiObject
    			}
    
    		# Try to kick off the inventory
    		Try {
    				Write-Host "Starting ConfigMgr Hardware Inventory..."
    				Invoke-WmiMethod -ComputerName $env:ComputerName -Namespace root\ccm -Class SMS_Client -Name TriggerSchedule -ArgumentList $strScanType -ErrorAction Stop | Out-Null
    			}
    			Catch 
    				{
    					Write-Host "Hardware Inventory failed for $env:ComputerName`: $_" -ForegroundColor Red
    				}
    	}
    
    
    
    # ***** End of Functions *****
    
    
    
    # ***** Process Script *****
    
    # ***** Set internal script variables *****
    $global:intBrowserExtensionCount = 1
    $strProfileListKey = 'Registry::HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*'
    $strProfilePathsList = Get-ItemProperty -Path $strProfileListKey | Select-Object -Property ProfileImagePath
    $strProfileRoot = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList").ProfilesDirectory
    
    # Record the script run time in a WMI formatted variable
    $dtScriptRunTime = New-Object -ComObject WbemScripting.SWbemDateTime
    $dtScriptRunTime.SetVarDate($(Get-Date))
    $dtScriptRunTime = $dtScriptRunTime.Value
    
    # Import .NET 4.5 compression utilities
    Add-Type -As System.IO.Compression.FileSystem | Out-Null
    
    
    # Echo the start info
    Write-Host "`n`nStarting Browser Inventory on $env:ComputerName at $(Get-Date -Format g)....`n"
    
    
    # Check/Create/Clear the custom WMI class
    Prepare-Wmi-Class
    
    
    # Go through the Windows user profiles and find each of the Chrome and Mozilla extensions
    $strProfilePathsList | Foreach-object {
    
    		If ($boolInventoryChrome -eq $true)
    			{
    				# Check for Chrome Extensions
    				InventoryExtensions $_.ProfileImagePath 'CHROME'
    			}
    			
    		If ($boolInventoryMozilla -eq $true)
    			{
    				# Check for Mozilla Extensions
    				InventoryExtensions $_.ProfileImagePath 'MOZILLA'
    			}
    		
    		If ($boolInventoryEdge -eq $true)
    			{
    				# Check for Edge Extensions
    				InventoryExtensions $_.ProfileImagePath 'EDGE'
    			}
    }
    
    
    
    # Kick off the ConfigMgr Inventory if any of the Browser Inventories was set to run and at least one extension was logged
    If (($boolInventoryChrome -eq $true -or $boolInventoryMozilla -eq $true -or $boolInventoryEdge -eq $true) -and $intBrowserExtensionCount -gt 1)
    	{
    		Write-Host "`nBrowser Extension Inventory recorded $($intBrowserExtensionCount - 1) extensions.  Requesting ConfigMgr Inventory update....`n"
    		StartConfigMgrInventory $false
    	}
    
    
    # Stop the Transcript
    Stop-Transcript


    Also, don't forget to import the MOF into ConfigMgr's Hardware Inventory settings in the Client Policies.  Here is the new MOF file info (don't copy the *):

    ***********************

    [ SMS_Report     (TRUE),
      SMS_Group_Name ("BrowserExtensionsV1"),
      SMS_Class_ID   ("BrowserExtensionsV1") ]

    class CM_BrowserExtensionsV1 : SMS_Class_Template
    {
        [SMS_Report (TRUE),key ]  uint32     Counter;
        [SMS_Report (TRUE)     ]  string     Name;
        [SMS_Report (TRUE)     ]  DateTime   FolderDate;
        [SMS_Report (TRUE)     ]  string     FolderName;
        [SMS_Report (TRUE)     ]  string     ProfilePath;
        [SMS_Report (TRUE)     ]  string     Browser;
        [SMS_Report (TRUE)     ]  DateTime   ScriptLastRan;
        [SMS_Report (TRUE)     ]  string     Version;
    };

    ********************

    Finally, I've also created an SSRS/ConfigMgr Report to query the data.  The report, MOF, and script are all available here:  https://1drv.ms/f/s!AiKoy1qLZeLEgpItqLMYJlCG3Owvlg

    Thank you so much to everyone who helped get me here.  You saved me a ton of time and I learned some cool Powershell tricks along the way.





    Wednesday, March 20, 2019 12:55 PM