Private/Kinds/BrowserExtension.ps1
|
# The BrowserExtension Kind: whether an application's browser extension is installed and switched on, in each browser and profile. # # A password manager, a telephony client, a signing tool: the part of it the User works # with is an extension in the browser, and "it does not fill in my passwords" is a # ticket about that extension - not installed in the profile the User works in, switched # off, or cut off from the desktop application it talks to. A Run said nothing of any of # it. Observed 2026-10-13 on a workstation: a password manager's extension in Chrome and # in Edge, each in the one profile, in two different versions, none in either of two # Firefox profiles; and its link to the desktop application registered for all three. # # What is read, for the User of the Run: # # - Chrome and Edge: for every profile, whether the folder of the extension is there # and which version it holds, and from the profile's preferences whether it is # switched off. Of the preferences only the entry of this one extension is read. # - Firefox: for every profile, the entry of the extension in extensions.json. # - The link to the desktop application ("native messaging"), where a Check Definition # names one: whether it is registered for each browser, and whether what it points # at is there. # # Nothing of what an extension stores is read, and nothing a User did in a browser. # Readable without admin rights. function Get-BrowserExtensionSetting { # One field of an entry that arrives as ConvertFrom-Json made it or as a hashtable. Pure. [CmdletBinding()] param([AllowNull()]$Entry, [Parameter(Mandatory)][string]$Name) if ($null -eq $Entry) { return $null } if ($Entry -is [System.Collections.IDictionary]) { if ($Entry.Contains($Name)) { return $Entry[$Name] } else { return $null } } $property = $Entry.PSObject.Properties[$Name] if ($property) { return $property.Value } $null } function Select-BrowserExtensionVersion { <# .SYNOPSIS The newest of the versions an extension's folder holds: "2026.9.2_0" beside "2026.8.0_0" is 2026.9.2. Pure. .DESCRIPTION A browser keeps the version before beside the new one until it has restarted, so two folders are no fault. The part behind the underscore counts installs of one version and is no part of it. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyCollection()][string[]]$Name) $read = @($Name | Where-Object { $_ } | ForEach-Object { $text = ("$_" -split '_')[0] $parsed = $null if (-not [version]::TryParse($text, [ref]$parsed)) { $parsed = [version]'0.0' } [pscustomobject]@{ Text = $text; Version = $parsed } } | Sort-Object Version -Descending) if ($read.Count) { return $read[0].Text } '' } function ConvertFrom-BrowserExtensionPreference { <# .SYNOPSIS Whether a Chromium profile's preferences say an extension is switched on: $true, $false, or $null where they do not name it. Pure. .DESCRIPTION Read by the rule the Browser Kind reads every extension by: Test-BrowserExtensionOn. #> [CmdletBinding()] param([AllowNull()]$Preference, [Parameter(Mandatory)][string]$Id) $entries = Get-BrowserPreferenceSetting -Document $Preference if (-not $entries.ContainsKey($Id)) { return $null } Test-BrowserExtensionOn -Entry $entries[$Id] } function Get-BrowserExtensionChromium { <# .SYNOPSIS One row for every profile of a Chromium browser: whether the extension is there, its version, and whether it is switched on. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Browser, [AllowEmptyString()][string]$Root = '', [AllowNull()][AllowEmptyString()][string]$Id) if (-not $Id) { return } # Which profiles there are is the one list both browser Kinds share. See BrowserProfile.ps1. foreach ($found in @(Get-BrowserProfile -Browser $Browser -Root $Root)) { $folder = Join-Path $found.Folder ('Extensions\{0}' -f $Id) $installed = [bool](Test-Path -LiteralPath $folder -PathType Container) $version = '' $enabled = $null if ($installed) { $version = Select-BrowserExtensionVersion -Name @(Get-ChildItem -LiteralPath $folder -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.Name }) foreach ($file in 'Secure Preferences', 'Preferences') { $path = Join-Path $found.Folder $file if ($null -ne $enabled -or -not (Test-Path -LiteralPath $path -PathType Leaf)) { continue } try { $enabled = ConvertFrom-BrowserExtensionPreference -Preference (Read-AppLogFile -Path $path | ConvertFrom-Json) -Id $Id } catch { $enabled = $null } } } [pscustomobject]@{ Browser = $Browser; Profile = $found.Profile; Installed = $installed; Version = $version; Enabled = $enabled } } } function Get-BrowserExtensionFirefox { [CmdletBinding()] [OutputType([psobject])] param([AllowEmptyString()][string]$Root = '', [AllowNull()][AllowEmptyString()][string]$Id) if (-not $Id) { return } foreach ($found in @(Get-BrowserProfile -Browser 'Firefox' -Root $Root)) { $file = Join-Path $found.Folder 'extensions.json' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue } # The name the User sees, not the folder's "abcd1234." before it. $name = $found.Profile $addon = $null try { $addon = @((Read-AppLogFile -Path $file | ConvertFrom-Json).addons | Where-Object { "$($_.id)" -eq $Id })[0] } catch { $addon = $null } if (-not $addon) { [pscustomobject]@{ Browser = 'Firefox'; Profile = $name; Installed = $false; Version = ''; Enabled = $null }; continue } [pscustomobject]@{ Browser = 'Firefox'; Profile = $name; Installed = $true; Version = "$($addon.version)" Enabled = Test-BrowserFirefoxExtensionOn -Addon $addon } } } function Get-BrowserExtensionHost { <# .SYNOPSIS Whether the link from the extension to the desktop application is registered for a browser, and whether the description it names and the program in it are there. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Browser, [Parameter(Mandatory)][string]$Key, [Parameter(Mandatory)][string]$Name) $manifest = '' foreach ($hive in 'HKCU:', 'HKLM:') { if ($manifest) { continue } $values = Get-ItemProperty -LiteralPath (Join-Path $hive (Join-Path $Key $Name)) -ErrorAction SilentlyContinue if ($values) { $manifest = "$($values.'(default)')".Trim() } } $manifestExists = [bool]($manifest -and (Test-Path -LiteralPath $manifest -PathType Leaf)) $target = $null if ($manifestExists) { try { $path = "$((Read-AppLogFile -Path $manifest | ConvertFrom-Json).path)".Trim() if ($path) { if (-not [IO.Path]::IsPathRooted($path)) { $path = Join-Path (Split-Path -Path $manifest -Parent) $path } $target = [bool](Test-Path -LiteralPath $path -PathType Leaf) } } catch { $target = $null } } [pscustomobject]@{ Browser = $Browser; Registered = [bool]$manifest; ManifestExists = $manifestExists; TargetExists = $target } } # What Get-BrowserExtensionData hands its Judge. See Private/DataShape.ps1. $script:BrowserExtensionDataShape = @{ App = 'Value' Title = 'Value' Profiles = 'List' HostName = 'Value' Hosts = 'List' } function Get-BrowserExtensionData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $ids = Get-Parameter $Parameters 'Ids' $null $name = "$(Get-Parameter $Parameters 'NativeHost' '')".Trim() # The profiles of the User of the Run, from the list the Browser Kind reads too. $rows = @( Get-BrowserExtensionChromium -Browser 'Chrome' -Id "$(Get-BrowserExtensionSetting $ids 'Chrome')" Get-BrowserExtensionChromium -Browser 'Edge' -Id "$(Get-BrowserExtensionSetting $ids 'Edge')" Get-BrowserExtensionFirefox -Id "$(Get-BrowserExtensionSetting $ids 'Firefox')" ) $hosts = @() if ($name) { $hosts = @( Get-BrowserExtensionHost -Browser 'Chrome' -Key 'Software\Google\Chrome\NativeMessagingHosts' -Name $name Get-BrowserExtensionHost -Browser 'Edge' -Key 'Software\Microsoft\Edge\NativeMessagingHosts' -Name $name Get-BrowserExtensionHost -Browser 'Firefox' -Key 'Software\Mozilla\NativeMessagingHosts' -Name $name ) } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.BrowserExtension' App = "$(Get-Parameter $Parameters 'App' '')" Title = "$(Get-Parameter $Parameters 'Title' '')" Profiles = $rows HostName = $name Hosts = $hosts } } function Get-BrowserExtensionTitle { # What the extension is called in the Report. Pure. [CmdletBinding()] param([AllowNull()]$Data) $title = "$(Get-DataProperty $Data 'Title')".Trim() if (-not $title) { $title = "$(Get-DataProperty $Data 'App')".Trim() } if (-not $title) { $title = Get-Text 'Value.BrowserExtension.Nameless' } $title } function New-BrowserExtensionInstallFinding { <# .SYNOPSIS Where the extension is installed and switched on: each browser and profile. .DESCRIPTION OK where it is on in at least one profile. Where it is nowhere, or off wherever it is, that is said at INFO: whether the User is meant to have it is not a thing a machine knows. #> [CmdletBinding()] param([AllowNull()]$Data) $title = Get-BrowserExtensionTitle -Data $Data $check = (Get-Text 'Check.BrowserExtension.Installed') -f $title $profiles = @((Get-DataCollection $Data 'Profiles') | Where-Object { $_ }) if (-not $profiles.Count) { return New-Finding -Category Apps -Check $check -Severity INFO -Value (Get-Text 'Value.BrowserExtension.NoBrowser') -Hint (Get-Text 'Hint.BrowserExtension.NoBrowser') } $items = @($profiles | ForEach-Object { $where = (Get-Text 'Value.BrowserExtension.Where') -f "$(Get-DataProperty $_ 'Browser')", "$(Get-DataProperty $_ 'Profile')" if (-not [bool](Get-DataProperty $_ 'Installed')) { return (Get-Text 'Value.BrowserExtension.Absent') -f $where } $enabled = Get-DataProperty $_ 'Enabled' $state = Get-Text 'Value.BrowserExtension.StateUnknown' if ($enabled -eq $true) { $state = Get-Text 'Value.BrowserExtension.On' } elseif ($enabled -eq $false) { $state = Get-Text 'Value.BrowserExtension.Off' } (Get-Text 'Value.BrowserExtension.Present') -f $where, "$(Get-DataProperty $_ 'Version')", $state }) $value = (Get-Text 'Value.BrowserExtension.Profiles') -f ($items -join '; ') $reference = @(New-Reference -Section 'Title.BrowserExtension.Profiles') $installed = @($profiles | Where-Object { [bool](Get-DataProperty $_ 'Installed') }) $on = @($installed | Where-Object { (Get-DataProperty $_ 'Enabled') -ne $false }) if (-not $installed.Count) { return New-Finding -Category Apps -Check $check -Severity INFO -Value ('{0} | {1}' -f (Get-Text 'Value.BrowserExtension.Nowhere'), $value) ` -Meaning (Get-Text 'Meaning.BrowserExtension.Nowhere') -Hint (Get-Text 'Hint.BrowserExtension.Nowhere') -Reference $reference } if (-not $on.Count) { return New-Finding -Category Apps -Check $check -Severity INFO -Value ('{0} | {1}' -f (Get-Text 'Value.BrowserExtension.OffEverywhere'), $value) ` -Meaning (Get-Text 'Meaning.BrowserExtension.Off') -Hint (Get-Text 'Hint.BrowserExtension.Off') -Reference $reference } New-Finding -Category Apps -Check $check -Severity OK -Value $value -Reference $reference } function New-BrowserExtensionHostFinding { <# .SYNOPSIS Whether the link from the extension to the desktop application is in order, for each browser. Nothing where a Check Definition names no such link. .DESCRIPTION A WARN where a link is registered and what it points at is gone: that is what an uninstall or a moved installation leaves, and the extension then fails without saying why. A link that is not registered is one nobody switched on, and no fault. #> [CmdletBinding()] param([AllowNull()]$Data) if (-not "$(Get-DataProperty $Data 'HostName')".Trim()) { return } $title = Get-BrowserExtensionTitle -Data $Data $check = (Get-Text 'Check.BrowserExtension.Host') -f $title $hosts = @((Get-DataCollection $Data 'Hosts') | Where-Object { $_ }) $registered = @($hosts | Where-Object { [bool](Get-DataProperty $_ 'Registered') }) if (-not $registered.Count) { return New-Finding -Category Apps -Check $check -Severity OK -Value (Get-Text 'Value.BrowserExtension.HostNone') } $broken = @($registered | Where-Object { -not [bool](Get-DataProperty $_ 'ManifestExists') -or (Get-DataProperty $_ 'TargetExists') -eq $false }) $items = @($registered | ForEach-Object { $state = Get-Text 'Value.BrowserExtension.HostFine' if (-not [bool](Get-DataProperty $_ 'ManifestExists')) { $state = Get-Text 'Value.BrowserExtension.HostNoManifest' } elseif ((Get-DataProperty $_ 'TargetExists') -eq $false) { $state = Get-Text 'Value.BrowserExtension.HostNoTarget' } (Get-Text 'Value.BrowserExtension.HostItem') -f "$(Get-DataProperty $_ 'Browser')", $state }) $value = (Get-Text 'Value.BrowserExtension.Hosts') -f ($items -join '; ') if ($broken.Count) { return New-Finding -Category Apps -Check $check -Severity WARN -Value $value ` -Meaning (Get-Text 'Meaning.BrowserExtension.HostBroken') -Hint ((Get-Text 'Hint.BrowserExtension.HostBroken') -f $title) } New-Finding -Category Apps -Check $check -Severity OK -Value $value } function ConvertTo-BrowserExtensionFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) New-BrowserExtensionInstallFinding -Data $Data New-BrowserExtensionHostFinding -Data $Data } function ConvertTo-BrowserExtensionSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $profiles = @((Get-DataCollection $Data 'Profiles') | Where-Object { $_ }) if (-not $profiles.Count) { return } $linked = @{} foreach ($one in @((Get-DataCollection $Data 'Hosts') | Where-Object { $_ })) { $linked["$(Get-DataProperty $one 'Browser')"] = $one } $named = [bool]"$(Get-DataProperty $Data 'HostName')".Trim() New-Section -Title (Get-Text 'Title.BrowserExtension.Profiles') -Help (Get-Text 'Help.BrowserExtension.Profiles') -Row @( $profiles | ForEach-Object { $installed = [bool](Get-DataProperty $_ 'Installed') $row = [ordered]@{} $row[(Get-Text 'Column.BrowserExtension.Browser')] = "$(Get-DataProperty $_ 'Browser')" $row[(Get-Text 'Column.BrowserExtension.Profile')] = "$(Get-DataProperty $_ 'Profile')" $row[(Get-Text 'Column.BrowserExtension.Installed')] = Get-TriStateText $installed $row[(Get-Text 'Column.BrowserExtension.Version')] = "$(Get-DataProperty $_ 'Version')" $row[(Get-Text 'Column.BrowserExtension.Enabled')] = $(if ($installed) { Get-TriStateText (Get-DataProperty $_ 'Enabled') } else { '' }) if ($named) { $link = $linked["$(Get-DataProperty $_ 'Browser')"] $row[(Get-Text 'Column.BrowserExtension.Host')] = Get-TriStateText $(if ($link) { [bool](Get-DataProperty $link 'Registered') } else { $null }) } [pscustomobject]$row } ) } |