Using Powershell to get installed Edge and Chrome extensions

I want to start off with acknowledging that I know, but don’t have the ability, to manage Google Chrome in an organizational setting through Google Cloud which produces nice reports of extensions installed — and this can even be done with M365 Defender. My dilemma is that for some networks I do not have the ability to do this, so I needed an alternative way. This is what I’m developing.

I found an article on Spiceworks that someone created a PowerShell script already while I was trying to find the original article I got the inspiration from. I learned a little about how Chrome extensions use manifest.json and that “name” key doesn’t always, as of Manifest V2, give the actual extension name.

Reading through Chrome.18n documentation on how the manifest is laid out and internationalization structure, I was able to figure out a way to programmatically look things up.

In manifest.json the following keys are required:

default_locale is required. It can be any value, really, but standards are en_US, en, ru, etc.

name is required. It can be the actual name of the extension, but in Manifest V3, it is the string to translate which is preceded by __MSG_ and ends with __ (two underscores). The value between could be anything, for example __MSG_name__ or __MSG_AppName__.

So my thought is to look up the name key, check for __MSG_ and also the default_locale if the name key matches. This then lets me find messages.json which I can use the __MSG_name__ to find the actual extension name.

Disclaimer: This is my script so far; my PowerShell sucks. But it works for me. I do know I need to handle and log errors (unavailable computers, etc.) and not rely on -ErrorAction SilentlyContinue.

October 2024: I just realized this does not capture Developer mode unpacked/packed extensions that may be installed.

# Ensure necessary modules are loaded
Import-Module ActiveDirectory

# Define an array to store the extensions
$extensions = @()

# Define a hash table of browsers/paths
$browsers = @{
    "Edge" = "AppData\Local\Microsoft\Edge\User Data\Default\Extensions"
    "Chrome" = "AppData\Local\Google\Chrome\User Data\Default\Extensions"
}

Clear-Host

# Get a list of AD computers (handling domain and local environments)
try {
    $computers = Get-ADComputer -Filter * -Credential (Get-Credential)
} catch {
    Write-Host "Could not retrieve AD computers. Running on local machine."
    $computers = @([pscustomobject]@{ Name = $env:COMPUTERNAME })
}

foreach ($computer in $computers) {
    $computerName = $computer.Name
    $computerPath = "\\$computerName\c$\Users"

    Write-Host "******************** $computerName **********************"

    # Get a list of users excluding 'Public' and 'Default'
    try {
        $users = Get-ChildItem -Path $computerPath -ErrorAction SilentlyContinue |
                 Where-Object { $_.PSIsContainer -and $_.Name -notmatch '^(Public|Default|All Users|Default User|defaultuser0|WDAGUtilityAccount)$' }
    } catch {
        Write-Host "Failed to access users on $computerName. Skipping."
        continue
    }

    foreach ($user in $users) {
        $userName = $user.Name
        Write-Host "Checking $userName : $computerName`n"

        foreach ($browser in $browsers.Keys) {
            $browserPath = $browsers[$browser]
            $extensionPath = Join-Path -Path $computerPath -ChildPath "$userName\$browserPath"

            # Check if the Extensions folder exists
            if (Test-Path -Path $extensionPath -ErrorAction SilentlyContinue) {
                $manifestFiles = Get-ChildItem -Path $extensionPath -Filter manifest.json -Recurse -ErrorAction SilentlyContinue

                foreach ($manifestFile in $manifestFiles) {
                    $extDir = $manifestFile.DirectoryName
                    $extID = $manifestFile.Directory.Parent.Name
                    $manifestData = Get-Content -Path $manifestFile.FullName | ConvertFrom-Json

                    $extensionName = if ($manifestData.name -like "*__MSG_*") {
                        $msgJSON = Join-Path -Path $extDir -ChildPath "_locales\$($manifestData.default_locale)\messages.json"
                        $extNameKey = $manifestData.name -replace "__MSG_", "" -replace "__", ""
                        (Get-Content -Encoding UTF8 -Path $msgJSON | ConvertFrom-Json).$extNameKey.message
                    } else {
                        $manifestData.name
                    }

                    $extensions += [PSCustomObject]@{
                        ComputerName  = $computerName
                        UserName      = $userName
                        ExtensionName = $extensionName
                        ExtensionID   = $extID
                        Browser       = $browser
                    }
                    Write-Host "[$browser]: $extensionName ($extID)"
                }
                Write-Host "`n"
            }
        }
    }
}

# Export the extensions to a CSV file
$extensions | Export-Csv -Path "browser_extension_report.csv" -Encoding UTF8 -NoTypeInformation

Once it is done, a report should be created as a CSV. Here’s an example. It lists each extension installed for every user per machine scanned.

Managing Extensions in Google Chrome

What I’m working on now is a way to uninstall these after I review installed extensions and determine which ones I want to remove and block from re-installation. This is still a work in progress, here are some of my notes.

This is for removing an app, not an extension. See: Extension and App Types

"C:\Program Files\Google\Chrome\Application\chrome.exe" --profile-directory=Default --uninstall-app-id=EXTENSION_ID;

I’m also seeing some references to registry locations.

HKEY_USERS\Group Policy Objects\Machine\Software\Policies\Google\Chrome\ExtensionInstallForcelist
HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist

Looks like I need to add the extension to the ExtensionInstallForceList and then remove it.

Add ForceInstall

New-ItemProperty -Path "HKLM\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist " -Name "1" -Value EXTENSION_ID

Remove ForceInstall

Note: Need to specify the same value that was added in the Add step. This does not actually remove the extension if it is installed.

Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist" -Name "EXTENSION_NAME"

Remove (uninstall) the Extension

Could it be as simple as deleting the actual extension folder? sigh

Remove-Item -Path "C:\users\username\appdata\local\google\chrome\user data\default\extension\EXTENSION_ID" -Recurse

Block Extension

Use * for the EXTENSION_NAME and value of 1 to block all extension installations.

Note: -Name is an integer value being placed in the registry. To add another blocked extension, make sure to increment to the another value.

New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallBlocklist" -Name "1" -Value EXTENSION_ID;

Allow Extension

New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallAllowlist" -Name "1" -Value EXTENSION_ID;

I’ll end up doing this by Group Policy instead of per-computer in Active Directory environments. I’ll block all, and permit only approved extensions.

Group Policy (Google Policy Templates): Computer Configuration > Administrative Templates > Class Administrative Templates > Google > Chrome > Extensions

Delete saved passwords for Chrome and Edge from the command line

After implementing a Group Policy to prohibit saving of passwords in Google Chrome and Microsoft Edge, the previously saved passwords are still on the system. To remove these from multiple systems, a simple script can be deployed via GPO at User Logon to do the work. Otherwise, on a case-by-case basis, the passwords can be cleared by going into each browser’s settings and then the passwords section to clear saved passwords.

@echo off

taskkill /f /im msedge.exe
taskkill /f /im chrome.exe

del "%LocalAppData%\Google\Chrome\User Data\Default\Login Data" /q
del "%LocalAppData%\microsoft\edge\User Data\Default\Login Data" /q

Website Performance Analysis and Graphing – Debian NodeJS + Puppeteer + Cacti

Looking to capture some performance metrics for website from the Linux command line and eventually get it into Cacti (RRD).

Here are my scattered notes on this process. I’m not very familiar with NodeJS stuff, so I’m documenting from installation of NodeJS on Debian 11 to creating the project.

Install NodeJS, Puppeteer and Chromium headless on Debian 11

Install NodeJS

curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash -
apt install -y nodejs

Create Project

mkdir test_project
cd test_project
npm init

Install NodeJS Puppeteer

npm i puppeteer --save

Install Debian dependencies for Chromium

See: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix

This is what I needed to grab:

apt install libatk-bridge2.0-0 libatk1.0-0 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxrandr2 libxrender1 libgbm1 libxkbcommon-x11-0

Write the Application

This is basic idea copied and modified from something I found online on SO. It takes an argument, the website, passed.

Create index.js:

const puppeteer = require('puppeteer');

(async () => {
        const browser = await puppeteer.launch({
        ignoreDefaultArgs: ['--no-sandbox'],
});
        const page = await browser.newPage();

        const t1 = Date.now();
        await page.goto(process.argv[2], { waitUntil: 'networkidle0'});
        const diff1 = Date.now() - t1;

        await browser.close();
        console.log(`Time: ${diff1}ms`);
})();

To run it:

node app.js https://google.com

Example output:

Time: 1201ms

Integrate with Cacti

TODO

The basic idea is to be able to call the node app.js https://website/ and have it return a metric (milliseconds) that can be stored into an RRD and then graphed upon. Concern would be ensuring that the poller allows for script completion — I’m not sure what would happen if node can’t complete the job before the poller times out.

Other Methods

Some things I scoured from the internet.

Curl

curl -s -w 'Testing Website Response Time for :%{url_effective}\n\nLookup Time:\t\t%{time_namelookup}\nConnect Time:\t\t%{time_connect}\nAppCon Time:\t\t%{time_appconnect}\nRedirect Time:\t\t%{time_redirect}\nPre-transfer Time:\t%{time_pretransfer}\nStart-transfer Time:\t%{time_starttransfer}\n\nTotal Time:\t\t%{time_total}\n' -o /dev/null https://example.com/wp-json/wc/v3
Testing Website Response Time for :https://example.com/wp-json/wc/v3

Lookup Time: 0.004972
Connect Time: 0.053358
AppCon Time: 0.112053
Redirect Time: 0.000000
Pre-transfer Time: 0.112155
Start-transfer Time: 0.746088

Total Time: 0.851602

Cacti: Using Cacti to monitor web page loading

The AskAboutPHP.com has a PHP script to grab some info (not rendering, but at least some of the connection timings) and walks through how to integrate with Cacti for graphing. There are 3 parts:

Part 1: http://www.askaboutphp.com/2008/09/17/cacti-using-cacti-to-monitor-web-page-loading-part-1/

Part 2: http://www.askaboutphp.com/2008/09/19/cacti-using-cacti-to-monitor-web-page-loading-part-2/

Part 3: http://www.askaboutphp.com/2008/09/19/cacti-using-cacti-to-monitor-web-page-loading-part-3/

For modern systems, you’ll need to fix up the pageload-agent.php file to fix the line and remove deprecated and removed function eregi to match the following (line 10):

if (!preg_match('/^https?:\/\//', $url_argv, $matches)) {
                $url_argv = "https://$url_argv";
        }

ChromeOS Flex Download

Direct download of the ChromeOS Flex bin files are available at Chromium Dash.

The ChromeOS Flex recovery does not work on Linux, but you can use the following to workaround that.

Here are the instructions for the Linux script:   

  1. On the Linux computer, download the Recovery Tool.
  2. Change the script permissions to allow execution with the following command: 
    sudo chmod 755 linux_recovery.sh
  3. Run the script with root privileges with the following command: 
    sudo bash linux_recovery.sh --config https://dl.google.com/dl/edgedl/chromeos/recovery/cloudready_recovery.conf
  4. Follow the on-screen instructions to create recovery media.
  5. To continue to recover your Chromebook, follow the steps above.  
--config https://dl.google.com/dl/edgedl/chromeos/recovery/cloudready_recovery.conf

Here are a couple photos of me running this on a Lenovo ThinkCentre M70z 16GB RAM i5-10400U 6 core with 512GB SSD.

Cleanup All User Chrome Cache

This script will clean all Chrome cache for all users on a Windows 7 or newer system.

# Define the base directory for user profiles
$usersPath = "C:\Users"
$totalBeforeCleanup = 0
$totalAfterCleanup = 0

# Loop through each user folder in the Users directory
Get-ChildItem -Path $usersPath -Directory | ForEach-Object {
$chromeCachePath = Join-Path -Path $_.FullName -ChildPath "AppData\Local\Google\Chrome\User Data\Default\Cache"
$userBeforeSize = 0
$userAfterSize = 0

# Check if the Chrome Cache folder exists
if (Test-Path -Path $chromeCachePath) {
# Calculate size before cleanup
$userBeforeSize = (Get-ChildItem -Path $chromeCachePath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1MB
$totalBeforeCleanup += $userBeforeSize

# Remove all files in the Chrome Cache folder
Get-ChildItem -Path $chromeCachePath -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue

# Calculate size after cleanup (to check if any files were in use and not deleted)
$userAfterSize = (Get-ChildItem -Path $chromeCachePath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1MB
$totalAfterCleanup += $userAfterSize

# Output results for each user
Write-Output ("User: {0}`nCache size before cleanup: {1:N2} MB`nCache size after cleanup: {2:N2} MB`nFreed up: {3:N2} MB" -f $_.Name, $userBeforeSize, $userAfterSize, ($userBeforeSize - $userAfterSize))
} else {
Write-Output "No Chrome cache folder found for user: $($_.Name)"
}
}

# Output total cleanup summary
$totalCleanedUp = $totalBeforeCleanup - $totalAfterCleanup
Write-Output ("`nTotal Chrome cache size before cleanup: {0:N2} MB" -f $totalBeforeCleanup)
Write-Output ("Total Chrome cache size after cleanup: {0:N2} MB" -f $totalAfterCleanup)
Write-Output ("Total freed up: {0:N2} MB" -f $totalCleanedUp)