Files
autofirmer-expanded/scripts/install-extension-policy.ps1
T
Brandon LiandClaude Opus 5 e42e729189 Skip Edge on unmanaged devices instead of writing a dead policy
Unmanaged Edge installs extensions from the Microsoft Store and nowhere else:
it reads ExtensionInstallForcelist, blanks the value to [BLOCKED]<id>, and
reports an invalid extension id forever after. Writing that key buys nothing
and costs a permanent policy error plus a "Managed by your organization"
badge, so the script now checks dsregcmd and skips Edge with the two ways
round it. -Force writes it anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:25:39 -05:00

219 lines
8.7 KiB
PowerShell

<#
.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,
# Write the Edge policy even though Edge will refuse to act on it.
[switch]$Force
)
$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
}
<#
Edge refuses to install an extension it did not get from the Microsoft Store
unless the device is managed — domain-joined, Intune-enrolled or Entra-joined.
The policy is read and then blanked: edge://policy shows the value as
"[BLOCKED]..." with "This device isn't managed by your organization", and an
"Invalid extension ID" error from parsing that prefix.
Chrome and Brave have no such restriction. So on an unmanaged machine there
is nothing to be gained by writing Edge's key, and a real cost: Edge starts
reporting "Managed by your organization" and shows a policy error forever.
#>
function Test-DeviceManaged {
try {
$status = & dsregcmd /status 2>$null | Out-String
return ($status -match 'AzureAdJoined\s*:\s*YES') -or
($status -match 'DomainJoined\s*:\s*YES') -or
($status -match 'EnterpriseJoined\s*:\s*YES')
} catch {
return $false # no dsregcmd is itself a fair sign this is not managed
}
}
$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.'
}
if (-not $Remove -and ($targets -contains 'Edge') -and -not $Force -and -not (Test-DeviceManaged)) {
Write-Warning @'
Edge is skipped: this device is not managed (not domain-joined, Intune-enrolled
or Entra-joined), and unmanaged Edge only installs extensions from the Microsoft
Store. It would read the policy and blank the value.
Use Chrome or Brave for the update-server install, or load extension\ unpacked
in Edge - an unpacked copy still reloads itself when the dashboard reports a
newer version. -Force writes the key anyway.
'@
$targets = @($targets | Where-Object { $_ -ne 'Edge' })
if (-not $targets) { exit 1 }
}
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.
"@