<# .SYNOPSIS Install the AutoFirmer extension from our own update server, via policy. .DESCRIPTION Chrome will not side-load a CRX on Windows — dropping one on chrome://extensions is refused — and it will not update an unpacked folder. The supported way to run an extension you host yourself is a policy: ExtensionInstallForcelist names the extension id and the update manifest, and from then on the browser installs it, keeps it updated on its own schedule, and re-installs it if it goes missing. Edge and Brave are Chromium too: same CRX, same policy, same update manifest, only a different registry vendor key. By default this configures every one of the three that is actually installed. Defaults to the current user's policy hive, which needs no elevation. -Scope Machine writes to HKLM instead, for every account on the box; that one does need an elevated shell. Where both exist, HKLM wins. After this runs: * Remove the unpacked copy from chrome://extensions. It has the same id now, and the browser will not load both. * Restart the browser, or open chrome://policy and press "Reload policies". * A force-installed extension cannot be disabled or removed from the extensions page. Re-run with -Remove to take it off. .EXAMPLE powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 .EXAMPLE powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 -Browser Edge -Scope Machine #> [CmdletBinding()] param( [ValidateSet('All', 'Chrome', 'Edge', 'Brave')] [string]$Browser = 'All', [ValidateSet('User', 'Machine')] [string]$Scope = 'User', # Derived from the signing key on the droplet. It only changes if the key does. [string]$Id = 'lkjbblagjjbldlkeaaffhiopdlhniaan', [string]$UpdateUrl = 'https://ext.juicerroom.com/updates.xml', [switch]$Remove ) $ErrorActionPreference = 'Stop' # Vendor key under SOFTWARE\Policies, and the executable each one registers. $vendors = @{ Chrome = 'Google\Chrome' Edge = 'Microsoft\Edge' Brave = 'BraveSoftware\Brave' } $exes = @{ Chrome = 'chrome.exe' Edge = 'msedge.exe' Brave = 'brave.exe' } # Where each one actually lands. Edge and Chrome are 32-bit installers on a # 64-bit box, which is also why the registry probe below has to look inside # WOW6432Node: a 64-bit PowerShell reading HKLM:\SOFTWARE sees the 64-bit view # and misses everything a 32-bit installer wrote. $installPaths = @{ Chrome = @("$env:ProgramFiles\Google\Chrome\Application\chrome.exe", "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe", "$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe") Edge = @("${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe") Brave = @("$env:ProgramFiles\BraveSoftware\Brave-Browser\Application\brave.exe", "${env:ProgramFiles(x86)}\BraveSoftware\Brave-Browser\Application\brave.exe", "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\Application\brave.exe") } function Test-BrowserInstalled([string]$Name) { foreach ($exe in $installPaths[$Name]) { if ($exe -and (Test-Path $exe)) { return $true } } # App Paths as a backstop, for an install somewhere unusual. $keys = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($exes[$Name])", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\$($exes[$Name])", "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($exes[$Name])" ) foreach ($key in $keys) { if (Test-Path $key) { return $true } } return $false } $hive = if ($Scope -eq 'Machine') { 'HKLM:' } else { 'HKCU:' } if ($Scope -eq 'Machine') { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() if (-not ([Security.Principal.WindowsPrincipal]$identity).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this from an elevated PowerShell for -Scope Machine, or drop the flag to install it for this user only.' } } $targets = if ($Browser -eq 'All') { @('Chrome', 'Edge', 'Brave') | Where-Object { Test-BrowserInstalled $_ } } else { @($Browser) } if (-not $targets) { throw 'Found no Chrome, Edge or Brave installation. Name one with -Browser to configure it regardless of what this can see.' } Write-Host "Configuring: $($targets -join ', ')" $entry = "$Id;$UpdateUrl" foreach ($target in $targets) { $key = "$hive\SOFTWARE\Policies\$($vendors[$target])\ExtensionInstallForcelist" if ($Remove) { if (-not (Test-Path $key)) { Write-Host "$target : nothing to remove — no $Scope forcelist." continue } $props = Get-ItemProperty $key $found = $false foreach ($name in ($props.PSObject.Properties.Name | Where-Object { $_ -notlike 'PS*' })) { if ($props.$name -like "$Id;*") { Remove-ItemProperty -Path $key -Name $name Write-Host "$target : removed entry $name ($($props.$name))" $found = $true } } if (-not $found) { Write-Host "$target : nothing to remove — $Id was not in the $Scope forcelist." } continue } if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } # The forcelist is a list expressed as numbered values, so adding to it means # finding a free number rather than overwriting whatever is already there — # another extension may well be in the list. $props = Get-ItemProperty $key $existing = $props.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' -and $_.Value -like "$Id;*" } if ($existing) { Set-ItemProperty -Path $key -Name $existing.Name -Value $entry Write-Host "$target : updated $Scope forcelist entry $($existing.Name)" } else { $used = $props.PSObject.Properties.Name | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ } $slot = if ($used) { (($used | Measure-Object -Maximum).Maximum + 1) } else { 1 } New-ItemProperty -Path $key -Name "$slot" -Value $entry -PropertyType String -Force | Out-Null Write-Host "$target : added $Scope forcelist entry $slot" } Write-Host " $key" } if ($Remove) { Write-Host "`nRestart the browser to let the change take effect." exit 0 } Write-Host @" $entry Next: 1. Remove the unpacked copy from chrome://extensions (same id — both cannot load). 2. Restart the browser, or open its chrome://policy page and press "Reload policies". 3. The card should read "Installed by enterprise policy". From here the browser updates it on its own. The extension also asks for a check as soon as the dashboard reports a newer version, so a release normally lands in seconds rather than waiting out the browser's own cycle. "@