Private/Kinds/Browser.ps1
|
# The Browser Kind: how Edge, Chrome and Firefox last closed for the User of the Run, the crash reports each holds, the extensions somebody installed in them, how large each profile is, the policies set for them, and the WebView2 runtime. # # "The browser keeps closing" is a ticket a Report said nothing about: Edge was known as an # application, Chrome and Firefox only where a Check Definition named one of their # extensions. Browsers keep no log a Run could read. What they leave is state, and this # Kind reads these pieces of it: # # - Edge and Chrome: for every profile, how the browser last closed, as the profile's # preferences recorded it - normally, crashed, or ended with the session. # - Edge, Chrome and Firefox: for every profile, the extensions in it - name, version, # on or off, and how each came there. Of the preferences only these entries are kept; # of an extension nothing it stores. Its name may be looked up in its own files. # - Edge, Chrome and Firefox: how large every profile is and how much of it is cache, # measured as the User Profile Check measures, within a time limit. A size, never a # name of what is in it. # - The policies of the three, for the machine and for the User, from the registry and # Firefox's policy file; and the proxy they force, set beside the User's proxy and the # WinHTTP proxy the Network Check read. Those two are handed over as observed data and # not read again; where the Network Check did not run, the Finding says so. # - The crashes of the browsers and of WebView2 (msedgewebview2.exe) the Stability Check # read from the event log, handed over the same way: a crash at the last exit beside # crashes logged of the same browser is a WARN. And whether the WebView2 runtime that # new Outlook, Teams and the web add-ins of Office are drawn in is installed. # - Edge, Chrome and Firefox: the crash reports the browser holds, counted and dated by # their files. A report is never opened, copied or hashed: what is in it is the # browser's memory (ADR-0006). # # Firefox records how it last closed in no file a Run can read; its profiles are listed, # and of their last exit the Report says nothing rather than "normal". # # Which profiles there are is the list the BrowserExtension Kind reads too (BrowserProfile.ps1), # so the two never disagree. Only the profiles of the account the Run runs as; nothing of # another User. Readable without admin rights. # # Observed 2026-10-14 on a workstation: Edge open, and its profile in use recorded as # crashed - a Chromium browser writes "crashed" when it opens a profile and "normal" only # when it closes it. Read while the browser runs, every open profile looks crashed. The # Gatherer says whether the browser is running, and the Judge does not call that a crash. function ConvertFrom-BrowserPreferenceText { # A preferences document as an object, or the problem with it: 'Missing', 'Empty' or # 'Damaged'. Pure. [CmdletBinding()] param([AllowNull()]$Text) if ($null -eq $Text) { return 'Missing' } if (-not "$Text".Trim()) { return 'Empty' } $parsed = $null try { $parsed = $Text | ConvertFrom-Json -ErrorAction Stop } catch { return 'Damaged' } # A document of a profile is one object. A list, a number or a word is none. if ($null -eq $parsed -or $parsed -is [string] -or $parsed -is [ValueType] -or $parsed -is [array]) { return 'Damaged' } $parsed } function Get-BrowserPreferenceExit { # The last exit one document records, or $null where it records none. Pure. [CmdletBinding()] param([AllowNull()]$Document) $profile0 = Get-DataProperty $Document 'profile' if ($null -eq $profile0) { return $null } $type = "$(Get-DataProperty $profile0 'exit_type')".Trim() if ($type) { switch ($type) { 'Normal' { return 'Normal' } 'Crashed' { return 'Crashed' } 'SessionEnded' { return 'SessionEnded' } # A word a newer browser brings is none this module can put into words. default { return 'Unknown' } } } # The older way: whether it closed cleanly, and nothing else. $clean = Get-DataProperty $profile0 'exited_cleanly' if ($clean -is [bool]) { if ($clean) { return 'Normal' } else { return 'Crashed' } } $null } function ConvertFrom-BrowserPreferenceExtension { <# .SYNOPSIS The extensions two preference documents name, each entry joined from both. Pure. .DESCRIPTION A browser may write the entry of one extension half into each document: where it came from into the secure preferences, why it is off into the other. Joined field by field, and where both have a field the secure preferences have the last word: they are where a newer browser writes what it protects. An entry that names no place it came from ("location") is no installed extension: it is what is left of a removed one, a permission granted, a flag of the store. The location is handed on as the browser wrote it; what it means is the Judge's (Get-BrowserExtensionOrigin). The name may be a placeholder of the extension's own, "__MSG_appName__", or missing where a newer browser keeps no manifest in the preferences: the Gatherer looks it up in the extension's files. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Preference, [AllowNull()]$SecurePreference) $plain = Get-BrowserPreferenceSetting -Document $Preference $secure = Get-BrowserPreferenceSetting -Document $SecurePreference foreach ($id in @(@($plain.Keys) + @($secure.Keys) | Select-Object -Unique | Sort-Object)) { $entry = @{} foreach ($side in $plain, $secure) { if ($side.ContainsKey($id)) { foreach ($k in $side[$id].Keys) { $entry[$k] = $side[$id][$k] } } } $location = ConvertTo-Number $entry['location'] if ($null -eq $location) { continue } $manifest = $entry['manifest'] $version = "$(Get-DataProperty $manifest 'version')".Trim() if (-not $version) { # "<id>\2.4.1_0" or "2.4.1_0": the folder the version lies in. $last = @("$($entry['path'])" -split '[\\/]')[-1] $text = ($last -split '_')[0] if ($text -match '^\d+(\.\d+)*$') { $version = $text } } [pscustomobject]@{ Id = $id Name = "$(Get-DataProperty $manifest 'name')".Trim() Version = $version Enabled = Test-BrowserExtensionOn -Entry ([pscustomobject]$entry) Location = [int]$location ByDefault = [bool]($entry['was_installed_by_default'] -eq $true) Source = '' } } } function ConvertFrom-BrowserPreference { <# .SYNOPSIS What a Chromium profile's preferences say of it: how the browser last closed, and what kept them from saying. Pure. .DESCRIPTION Edge and Chrome keep two documents in a profile: "Preferences", and "Secure Preferences", where newer browsers keep what they protect against tampering. The last exit is in the first; the second is read where the first does not hold it. LastExit is Normal, Crashed, SessionEnded (the browser was closed by a sign-out or a shutdown) or Unknown. Problem is empty where the preferences could be read, and otherwise says why not: Missing, Empty or Damaged. Secure preferences that are missing or damaged are no problem: an older browser keeps none. Handed the text of the documents, never a path: the Gatherer reads the files. .PARAMETER Preference The text of "Preferences"; $null where the file is not there. .PARAMETER SecurePreference The text of "Secure Preferences"; $null where the file is not there. #> [CmdletBinding()] [OutputType([psobject])] # Not [string]: that makes '' of a $null, and a file that is not there is not one that # is empty. param([AllowNull()]$Preference, [AllowNull()]$SecurePreference) $document = ConvertFrom-BrowserPreferenceText -Text $Preference if ($document -is [string]) { return [pscustomobject]@{ LastExit = 'Unknown'; Problem = $document; Extensions = @() } } $secure = ConvertFrom-BrowserPreferenceText -Text $SecurePreference $exit = Get-BrowserPreferenceExit -Document $document if ($null -eq $exit -and $secure -isnot [string]) { $exit = Get-BrowserPreferenceExit -Document $secure } if ($null -eq $exit) { $exit = 'Unknown' } [pscustomobject]@{ LastExit = $exit Problem = '' Extensions = @(ConvertFrom-BrowserPreferenceExtension -Preference $document -SecurePreference $secure) } } function Read-BrowserPreferenceFile { # The text of one document of a profile, $null where the file is not there. Read while # the browser has it open: it keeps it open as long as it runs. [CmdletBinding()] param([Parameter(Mandatory)][string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } Read-AppLogFile -Path $Path } function Read-BrowserJsonFile { # A JSON file of a browser or an extension as an object, $null where it is not there or # is no JSON. For the files a name is looked up in, where nothing is lost without one. [CmdletBinding()] param([Parameter(Mandatory)][string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } try { Read-AppLogFile -Path $Path | ConvertFrom-Json -ErrorAction Stop } catch { $null } } function Resolve-BrowserExtensionName { <# .SYNOPSIS The name of a Chromium extension as the User sees it in the browser, where the preferences hold none or a placeholder like "__MSG_appName__". .DESCRIPTION An extension that is translated names a message in its manifest, and the message is in its own folder: _locales\<language>\messages.json. Read in German first, then in the language the extension says is its own, then in English. Where none of that answers, the name stays empty and the Report names the extension by its id. #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)][string]$ProfileFolder, [Parameter(Mandatory)]$Extension) $name = "$($Extension.Name)" if ($name -and $name -notmatch '^__MSG_.+__$') { return $name } $root = Join-Path $ProfileFolder ('Extensions\{0}' -f $Extension.Id) if (-not (Test-Path -LiteralPath $root -PathType Container)) { return '' } $folders = @(Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue) $folder = @($folders | Where-Object { ($_.Name -split '_')[0] -eq $Extension.Version })[0] if (-not $folder) { $folder = @($folders | Sort-Object Name -Descending)[0] } if (-not $folder) { return '' } $manifest = Read-BrowserJsonFile -Path (Join-Path $folder.FullName 'manifest.json') if (-not $name) { $name = "$(Get-DataProperty $manifest 'name')".Trim() } if ($name -notmatch '^__MSG_(.+)__$') { return $name } $key = $Matches[1] foreach ($language in @('de', "$(Get-DataProperty $manifest 'default_locale')", 'en', 'en_US') | Where-Object { $_ } | Select-Object -Unique) { $messages = Read-BrowserJsonFile -Path (Join-Path $folder.FullName ('_locales\{0}\messages.json' -f $language)) if ($null -eq $messages) { continue } # A message's key is matched without regard to case, as the browser matches it. $entry = @($messages.PSObject.Properties | Where-Object { $_.Name -eq $key })[0] if ($entry) { $said = "$(Get-DataProperty $entry.Value 'message')".Trim(); if ($said) { return $said } } } '' } function Get-BrowserFirefoxExtension { <# .SYNOPSIS The extensions and themes of one Firefox profile, as its extensions.json lists them, and why that list could not be read. .DESCRIPTION Read as the BrowserExtension Kind reads it (Test-BrowserFirefoxExtensionOn). What kind of add-on it is, where it lies and how it was installed are handed on as Firefox wrote them; what they mean, and which count, is the Judge's. A profile without the list has never had anything installed and is no problem. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Folder) $file = Join-Path $Folder 'extensions.json' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { return [pscustomobject]@{ Problem = ''; Reason = ''; Extensions = @() } } try { $text = Read-AppLogFile -Path $file } catch { return [pscustomobject]@{ Problem = 'ListUnreadable'; Reason = $_.Exception.Message; Extensions = @() } } try { $list = $text | ConvertFrom-Json -ErrorAction Stop } catch { return [pscustomobject]@{ Problem = 'ListDamaged'; Reason = ''; Extensions = @() } } $extensions = @(@(Get-DataProperty $list 'addons') | Where-Object { $_ } | ForEach-Object { [pscustomobject]@{ Id = "$($_.id)" Name = "$(Get-DataProperty (Get-DataProperty $_ 'defaultLocale') 'name')".Trim() Version = "$($_.version)" Enabled = Test-BrowserFirefoxExtensionOn -Addon $_ Location = "$($_.location)" ByDefault = $false Source = "$(Get-DataProperty (Get-DataProperty $_ 'installTelemetryInfo') 'source')" Type = "$($_.type)" } }) [pscustomobject]@{ Problem = ''; Reason = ''; Extensions = $extensions } } function Get-BrowserProfileReading { <# .SYNOPSIS One profile as the Judge reads it: its name, how it last closed, its extensions, and why that could not be read. .DESCRIPTION Only the record leaves this function, never the documents: the preferences hold far more than a last exit and the extensions, and none of the rest is any of a Report's business. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)]$Found) # Firefox keeps no last exit a Run can read: its profile is listed with its extensions. if ($Found.Engine -ne 'Chromium') { $listed = Get-BrowserFirefoxExtension -Folder $Found.Folder return [pscustomobject]@{ Profile = $Found.Profile; LastExit = 'Unknown'; Problem = $listed.Problem; Reason = $listed.Reason; Extensions = @($listed.Extensions) } } try { $preference = Read-BrowserPreferenceFile -Path (Join-Path $Found.Folder 'Preferences') $secure = $null try { $secure = Read-BrowserPreferenceFile -Path (Join-Path $Found.Folder 'Secure Preferences') } catch { $secure = $null } $record = ConvertFrom-BrowserPreference -Preference $preference -SecurePreference $secure } catch { return [pscustomobject]@{ Profile = $Found.Profile; LastExit = 'Unknown'; Problem = 'Unreadable'; Reason = $_.Exception.Message; Extensions = @() } } $extensions = @($record.Extensions | ForEach-Object { $one = $_ try { $one.Name = Resolve-BrowserExtensionName -ProfileFolder $Found.Folder -Extension $one } catch { $one.Name = '' } $one }) [pscustomobject]@{ Profile = $Found.Profile; LastExit = $record.LastExit; Problem = $record.Problem; Reason = ''; Extensions = $extensions } } function Get-BrowserCrashReport { <# .SYNOPSIS When each crash report a browser holds was written: the times of the files, and nothing of what is in them. .DESCRIPTION Edge and Chrome keep their reports in "Crashpad\reports" of their "User Data" folder, one .dmp each; whether one was sent is written into a file of Crashpad's own that is not read, so the reports a browser holds are counted, sent or not. Firefox keeps the reports it has not sent in "Crash Reports\pending". #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Folder) if (-not (Test-Path -LiteralPath $Folder -PathType Container)) { return [pscustomobject]@{ Read = $true; Reason = ''; Times = @() } } try { $times = @(Get-ChildItem -LiteralPath $Folder -Filter '*.dmp' -File -ErrorAction Stop | ForEach-Object { $_.LastWriteTime }) [pscustomobject]@{ Read = $true; Reason = ''; Times = $times } } catch { [pscustomobject]@{ Read = $false; Reason = $_.Exception.Message; Times = @() } } } # The folders of a Chromium profile its caches are kept in. They lie in the profile, and # what they hold counts in its size as well. $script:BrowserChromiumCache = @('Cache', 'Code Cache', 'GPUCache', 'Service Worker\CacheStorage', 'Service Worker\ScriptCache') function Measure-BrowserProfile { <# .SYNOPSIS The size of every profile and of its cache, with how each measurement ended, all of them together within the time given. .DESCRIPTION Measured by the User Profile Check's own measurement (Measure-UserProfileFolder), with its time limit and its "cut short": a profile of many gigabytes must not hold up the Run, and what was counted before the time ran out is said to be at least. A Chromium profile keeps its cache inside itself; the measurement counts what lies in a cache folder once, as the cache's, and the profile's row is the rest. Firefox keeps its cache beside the profile, under the local application data, in a folder of the same name. Decides nothing: a row per part of each profile, Part and State and Bytes, in the order of the profiles handed in. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Found, [int]$Seconds = 30) $profiles = @($Found | Where-Object { $_ }) if (-not $profiles.Count) { return } $local = [Environment]::GetFolderPath('LocalApplicationData') $folders = New-Object System.Collections.Generic.List[psobject] for ($i = 0; $i -lt $profiles.Count; $i++) { $one = $profiles[$i] $folders.Add([pscustomobject]@{ Id = "$i|Profile"; Path = $one.Folder }) if ($one.Engine -eq 'Firefox') { $folders.Add([pscustomobject]@{ Id = "$i|Cache"; Path = (Join-Path $local ('Mozilla\Firefox\Profiles\{0}' -f (Split-Path -Path $one.Folder -Leaf))) }) } else { foreach ($cache in $script:BrowserChromiumCache) { $folders.Add([pscustomobject]@{ Id = "$i|Cache"; Path = (Join-Path $one.Folder $cache) }) } } } $sizes = @{} foreach ($row in @(Measure-UserProfileFolder -Folder $folders.ToArray() -ProfilePath '' -Seconds $Seconds)) { $index, $part = "$($row.Id)" -split '\|', 2 if (-not $sizes.ContainsKey($index)) { $sizes[$index] = New-Object System.Collections.Generic.List[psobject] } $sizes[$index].Add([pscustomobject]@{ Part = $part; State = (Get-UserProfileMeasureStateName -State $row.State); Bytes = [long]$row.Bytes }) } for ($i = 0; $i -lt $profiles.Count; $i++) { [pscustomobject]@{ Folder = $profiles[$i].Folder; Size = @($(if ($sizes.ContainsKey("$i")) { $sizes["$i"].ToArray() })) } } } # Where each browser reads its policies, under HKLM for the machine and HKCU for the User. # Readable without admin rights: a policy is meant to be read by the program it binds. $script:BrowserPolicyKey = @{ Chrome = 'SOFTWARE\Policies\Google\Chrome' Edge = 'SOFTWARE\Policies\Microsoft\Edge' Firefox = 'SOFTWARE\Policies\Mozilla\Firefox' } function Get-BrowserPolicyRegistry { <# .SYNOPSIS Every policy value under one key and its subkeys, a row each: the browser, the scope, the name, and the value as the registry holds it. .DESCRIPTION A list policy is a subkey whose values are named 1, 2, 3: it is one row, its name the subkey's and its value the list. Any other subkey's values are rows named by their path below the key, "Proxy\Mode", as Firefox writes its nested policies. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][string]$Browser, [Parameter(Mandatory)][string]$Scope, [Parameter(Mandatory)][string]$Path, [AllowEmptyString()][string]$Prefix = '' ) if (-not (Test-Path -LiteralPath $Path)) { return } $key = Get-Item -LiteralPath $Path -ErrorAction Stop $names = @($key.GetValueNames() | Where-Object { $_ }) if ($Prefix -and $names.Count -and -not @($names | Where-Object { $_ -notmatch '^\d+$' }).Count) { $list = @($names | Sort-Object { [int]$_ } | ForEach-Object { "$($key.GetValue($_))" }) [pscustomobject]@{ Browser = $Browser; Scope = $Scope; Name = $Prefix; Value = $list } } else { foreach ($name in $names) { $value = $key.GetValue($name) if ($value -is [array]) { $value = @($value | ForEach-Object { "$_" }) } else { $value = "$value" } $named = $name if ($Prefix) { $named = '{0}\{1}' -f $Prefix, $name } [pscustomobject]@{ Browser = $Browser; Scope = $Scope; Name = $named; Value = $value } } } foreach ($sub in @($key.GetSubKeyNames())) { $below = $sub if ($Prefix) { $below = '{0}\{1}' -f $Prefix, $sub } Get-BrowserPolicyRegistry -Browser $Browser -Scope $Scope -Path (Join-Path $Path $sub) -Prefix $below } } function Get-BrowserFirefoxPolicyFile { <# .SYNOPSIS The policies of Firefox's own policy file, distribution\policies.json beside the program: a row each, a nested one as its JSON. #> [CmdletBinding()] [OutputType([psobject])] param() $program = Get-BrowserExecutable -Browser 'Firefox' if (-not $program) { return } $file = Join-Path (Split-Path -Path $program -Parent) 'distribution\policies.json' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { return } $document = Read-AppLogFile -Path $file | ConvertFrom-Json -ErrorAction Stop foreach ($one in @((Get-DataProperty $document 'policies').PSObject.Properties)) { $value = $one.Value if ($null -ne $value -and $value -isnot [string] -and $value -isnot [ValueType]) { $value = $value | ConvertTo-Json -Compress -Depth 10 } [pscustomobject]@{ Browser = 'Firefox'; Scope = 'Machine'; Name = $one.Name; Value = "$value" } } } function Get-BrowserPolicy { <# .SYNOPSIS Every browser policy that is set, for the machine and for the User, and whether they could be read. Decides nothing, including what a policy means. #> [CmdletBinding()] [OutputType([psobject])] param() try { $rows = @(foreach ($browser in 'Chrome', 'Edge', 'Firefox') { foreach ($scope in @(@{ Name = 'Machine'; Hive = 'HKLM:' }, @{ Name = 'User'; Hive = 'HKCU:' })) { Get-BrowserPolicyRegistry -Browser $browser -Scope $scope.Name -Path (Join-Path $scope.Hive $script:BrowserPolicyKey[$browser]) } if ($browser -eq 'Firefox') { Get-BrowserFirefoxPolicyFile } }) [pscustomobject]@{ Read = $true; Reason = ''; Policies = $rows } } catch { [pscustomobject]@{ Read = $false; Reason = $_.Exception.Message; Policies = @() } } } # The client id under which Edge's updater registers the WebView2 runtime it keeps, the # "Evergreen" runtime: its version is the value "pv". 0.0.0.0 or nothing is none. $script:BrowserWebView2Client = '{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' function Get-BrowserWebView2 { <# .SYNOPSIS The version of the WebView2 runtime installed for the machine and for the User, each as the updater registered it; '' where it registered none. .DESCRIPTION Machine-wide under the 32-bit view of the registry on a 64-bit Windows and under the plain one on a 32-bit Windows; for the User under HKCU. #> [CmdletBinding()] [OutputType([psobject])] param() $read = { param($Path) "$((Get-ItemProperty -LiteralPath $Path -ErrorAction SilentlyContinue).pv)".Trim() } $machine = & $read ('HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{0}' -f $script:BrowserWebView2Client) if (-not $machine) { $machine = & $read ('HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{0}' -f $script:BrowserWebView2Client) } [pscustomobject]@{ Machine = $machine; User = (& $read ('HKCU:\Software\Microsoft\EdgeUpdate\Clients\{0}' -f $script:BrowserWebView2Client)) } } function Get-BrowserData { <# .SYNOPSIS Every browser of the three: whether it is installed and running, its profiles of the User of the Run, the crash reports it holds; and the policies of all three. .DESCRIPTION MeasureSeconds (30) is how long the profiles of all browsers are measured for, together. A limit of this module, so that a Run is not held up by a profile of many gigabytes; a profile not counted in that time is said to be "at least". .PARAMETER Observed What the Checks before this one gathered, keyed by Kind. The proxy of the User and of WinHTTP come from the Network Check, the crashes of the event log from the Stability Check, copied as they are. #> [CmdletBinding()] [OutputType([psobject])] param( [hashtable]$Parameters = @{}, [AllowNull()][hashtable]$Observed = @{} ) $seconds = ConvertTo-Number (Get-Parameter $Parameters 'MeasureSeconds' 30) if ($null -eq $seconds -or $seconds -le 0) { $seconds = 30 } # The Session the Run is in: a browser counts as running where it runs in this one. $session = (Get-Process -Id $PID).SessionId $places = @(Get-BrowserProfileRoot) $policies = Get-BrowserPolicy $network = Get-ObservedKindData -Observed $Observed -Kind 'Network' $stability = Get-ObservedKindData -Observed $Observed -Kind 'Stability' $found = @(Get-BrowserProfile) $sizes = @{} foreach ($measured in @(Measure-BrowserProfile -Found $found -Seconds ([int]$seconds))) { $sizes[$measured.Folder] = $measured.Size } $browsers = foreach ($place in $places) { $program = $script:BrowserProgram[$place.Browser] -replace '\.exe$', '' $reports = Join-Path $place.Root 'Crashpad\reports' if ($place.Engine -eq 'Firefox') { $reports = Join-Path (Split-Path -Path $place.Root -Parent) 'Crash Reports\pending' } [pscustomobject]@{ Browser = $place.Browser Engine = $place.Engine # Installed where Windows knows where its program is: every installer of the # three registers it, so that "Run" finds it. Installed = [bool](Get-BrowserExecutable -Browser $place.Browser) Running = [bool]@(Get-Process -Name $program -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session }).Count Profiles = @($found | Where-Object { $_.Browser -eq $place.Browser } | ForEach-Object { $reading = Get-BrowserProfileReading -Found $_ $reading | Add-Member -NotePropertyName Size -NotePropertyValue @($sizes[$_.Folder]) -PassThru }) CrashReports = Get-BrowserCrashReport -Folder $reports } } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Browser' GatheredAt = Get-Date MeasureSeconds = $seconds Browsers = @($browsers) Policies = @($policies.Policies) PoliciesRead = $policies.Read PoliciesReason = $policies.Reason # The crashes the Stability Check read from the event log, copied unfiltered: which # are a browser's or WebView2's is the Judge's to say. Absent where it did not run. StabilityObserved = $null -ne $stability ObservedCrashes = Get-DataCollection $stability 'Crashes' CrashWindowDays = Get-DataProperty $stability 'Days' WebView2 = Get-BrowserWebView2 } | Add-NetworkProxyObserved -Network $network } function Get-BrowserExit { <# .SYNOPSIS How a profile last closed, as the Judge takes it: Normal, Crashed, SessionEnded, Open, Unknown, Unread, or '' for a browser that keeps no last exit. Pure. .DESCRIPTION Open is a crash recorded while the browser runs in the Run's Session: it writes "crashed" when it opens a profile and "normal" when it closes it, so the profile that is open says crashed and is not. Which profile is open, it does not say. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Browser, [AllowNull()]$Row) if ("$(Get-DataProperty $Browser 'Engine')" -ne 'Chromium') { return '' } if ("$(Get-DataProperty $Row 'Problem')") { return 'Unread' } $exit = "$(Get-DataProperty $Row 'LastExit')" if ($exit -eq 'Crashed' -and [bool](Get-DataProperty $Browser 'Running')) { return 'Open' } if ($exit -in 'Normal', 'Crashed', 'SessionEnded') { return $exit } 'Unknown' } function Get-BrowserExitText { # How a profile last closed, in words; '' for a browser that keeps no last exit. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Browser, [AllowNull()]$Row) switch (Get-BrowserExit -Browser $Browser -Row $Row) { '' { return '' } 'Unread' { return (Get-Text 'Value.Cell.NotRead') } 'Open' { return (Get-Text 'Value.Browser.Exit.Open') } 'Normal' { return (Get-Text 'Value.Browser.Exit.Normal') } 'Crashed' { return (Get-Text 'Value.Browser.Exit.Crashed') } 'SessionEnded' { return (Get-Text 'Value.Browser.Exit.SessionEnded') } default { return (Get-Text 'Value.Browser.Exit.Unknown') } } } function Get-BrowserProblemText { # Why a profile could not be read, in words. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Row) switch ("$(Get-DataProperty $Row 'Problem')") { '' { return '' } 'Missing' { return (Get-Text 'Value.Browser.Problem.Missing') } 'Empty' { return (Get-Text 'Value.Browser.Problem.Empty') } 'Damaged' { return (Get-Text 'Value.Browser.Problem.Damaged') } 'ListDamaged' { return (Get-Text 'Value.Browser.Problem.ListDamaged') } 'ListUnreadable' { return (Get-Text 'Value.Browser.Problem.ListUnreadable' "$(Get-DataProperty $Row 'Reason')") } default { return (Get-Text 'Value.Browser.Problem.Unreadable' "$(Get-DataProperty $Row 'Reason')") } } } function New-BrowserClosedFinding { <# .SYNOPSIS How each profile of a Chromium browser last closed. .DESCRIPTION A crash is said at INFO: one unclean exit is a forced shutdown or a power cut as often as a fault, and a browser that crashes again and again shows in the crash reports it holds. A profile that could not be read is said at INFO too, with why: a gap must not read like a clean result. #> [CmdletBinding()] param([AllowNull()]$Data, [AllowNull()]$Browser) $name = "$(Get-DataProperty $Browser 'Browser')" $profiles = @((Get-DataCollection $Browser 'Profiles')) $items = @($profiles | ForEach-Object { $said = Get-BrowserExitText -Browser $Browser -Row $_ if ((Get-BrowserExit -Browser $Browser -Row $_) -eq 'Unread') { $said = Get-Text 'Value.Browser.Unread' (Get-BrowserProblemText -Row $_) } Get-Text 'Value.Browser.ProfileItem' "$(Get-DataProperty $_ 'Profile')" $said }) $crashed = @($profiles | Where-Object { (Get-BrowserExit -Browser $Browser -Row $_) -eq 'Crashed' }) $unread = @($profiles | Where-Object { (Get-BrowserExit -Browser $Browser -Row $_) -eq 'Unread' }) $check = Get-Text 'Check.Browser.Closed' $name $value = $items -join '; ' $reference = @(New-Reference -Section 'Title.Browser.Profiles' -Argument $name) if (-not ($crashed.Count + $unread.Count)) { return New-Finding -Category Browser -Check $check -Severity OK -Value $value -Subject $name -Reference $reference } # A crash at the last exit beside crashes Windows logged of the same browser is no # power cut: a WARN, which leads to the Stability Check's table of the crashes and is # not judged a second time here. if ($crashed.Count) { $program = $script:BrowserProgram[$name] if (-not [bool](Get-DataProperty $Data 'StabilityObserved')) { $value = '{0} | {1}' -f $value, (Get-Text 'Value.Browser.CrashesNotCompared') } else { $logged = @(Select-BrowserLoggedCrash -Data $Data -Program $program) if ($logged.Count) { $value = '{0} | {1}' -f $value, (Get-Text 'Value.Browser.CrashesLogged' $logged.Count $program (Get-DataProperty $Data 'CrashWindowDays') $logged[0].Time) $reference += New-Reference -Section 'Title.Stability.ApplicationCrashesByApp' $hint = @(Get-Text 'Hint.Browser.CrashedAndLogged') if ($unread.Count) { $hint += Get-Text 'Hint.Browser.Unread' } return New-Finding -Category Browser -Check $check -Severity WARN -Value $value -Subject $name -Reference $reference -LastSeen $logged[0].Time ` -Meaning (Get-Text 'Meaning.Browser.CrashedAndLogged') -Hint ($hint -join ' | ') } } } $meaning = @(); $hint = @() if ($crashed.Count) { $meaning += Get-Text 'Meaning.Browser.Crashed'; $hint += Get-Text 'Hint.Browser.Crashed' } if ($unread.Count) { $hint += Get-Text 'Hint.Browser.Unread' } New-Finding -Category Browser -Check $check -Severity INFO -Value $value -Subject $name -Reference $reference ` -Meaning ($meaning -join ' | ') -Hint ($hint -join ' | ') } function New-BrowserCrashReportFinding { <# .SYNOPSIS The crash reports a browser holds: how many of the period, and the newest. .DESCRIPTION Each report is a crash, whether Windows logged it or not. One or a few are said at INFO; more than CrashReportWarnAbove in the period is a browser that crashes often, and a WARN. Reports older than the period are counted apart and are no matter. #> [CmdletBinding()] param([AllowNull()]$Browser, [AllowNull()]$AsOf, [double]$Days, [double]$WarnAbove) $name = "$(Get-DataProperty $Browser 'Browser')" $check = Get-Text 'Check.Browser.CrashReports' $name $held = Get-DataProperty $Browser 'CrashReports' if ($null -eq $held -or -not [bool](Get-DataProperty $held 'Read')) { return New-Finding -Category Browser -Check $check -Severity INFO -Subject $name ` -Value (Get-Text 'Value.Browser.Unread' "$(Get-DataProperty $held 'Reason')") -Hint (Get-Text 'Hint.Browser.CrashReportsUnread') } $times = @((Get-DataCollection $held 'Times') | ForEach-Object { ConvertTo-DataTime $_ } | Where-Object { $_ } | Sort-Object -Descending) if (-not $times.Count) { return New-Finding -Category Browser -Check $check -Severity OK -Subject $name -Value (Get-Text 'Value.Browser.CrashReportsNone') } $since = $null $asOfAt = ConvertTo-DataTime $AsOf if ($asOfAt) { $since = $asOfAt.AddDays(-$Days) } $recent = @($times | Where-Object { $null -eq $since -or $_ -ge $since }) if (-not $recent.Count) { return New-Finding -Category Browser -Check $check -Severity OK -Subject $name ` -Value (Get-Text 'Value.Browser.CrashReportsOlder' $times.Count $Days $times[0]) } $value = Get-Text 'Value.Browser.CrashReports' $recent.Count $Days $recent[0] if ($recent.Count -gt $WarnAbove) { return New-Finding -Category Browser -Check $check -Severity WARN -Subject $name -Value $value -LastSeen $recent[0] ` -Meaning (Get-Text 'Meaning.Browser.CrashReports') -Hint (Get-Text 'Hint.Browser.CrashReportsMany') } New-Finding -Category Browser -Check $check -Severity INFO -Subject $name -Value $value -LastSeen $recent[0] ` -Meaning (Get-Text 'Meaning.Browser.CrashReports') -Hint (Get-Text 'Hint.Browser.CrashReports') } # Where a Chromium extension came from, by the number the browser writes as its "location". # 5 and 10 are parts of the browser itself; 7 and 9 a policy put there; 4 is a folder # loaded in developer mode and 8 one named on the command line; 2, 3 and 6 another program # put there, by a file, by the registry or by a download it named. 1 is the store. $script:BrowserChromiumOrigin = @{ 1 = 'Store'; 2 = 'Program'; 3 = 'Program'; 4 = 'Developer'; 5 = 'BuiltIn'; 6 = 'Program' 7 = 'Policy'; 8 = 'Developer'; 9 = 'Policy'; 10 = 'BuiltIn' } # The same for Firefox, by the place it writes an extension came from. $script:BrowserFirefoxOrigin = @{ 'app-profile' = 'Store'; 'temporary' = 'Developer' 'app-builtin' = 'BuiltIn'; 'app-builtin-addons' = 'BuiltIn'; 'app-system-defaults' = 'BuiltIn'; 'app-system-addons' = 'BuiltIn' 'app-global' = 'Program'; 'app-system-share' = 'Program'; 'app-system-local' = 'Program'; 'app-system-user' = 'Program' 'winreg-app-user' = 'Program'; 'winreg-app-global' = 'Program' } function Get-BrowserExtensionOrigin { <# .SYNOPSIS How an extension came into the browser: Store, Policy, Developer, Program, BuiltIn, or Unknown for a place this module has no word for. Pure. .DESCRIPTION BuiltIn is what the browser brings itself, also what it installed by default from a list of its own (Chrome's "Google Docs Offline" comes as a download from another program, and was put there by Chrome). A Firefox extension a policy installed lies where the User's own lie, and says so in how it was installed. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Browser, [AllowNull()]$Extension) $location = Get-DataProperty $Extension 'Location' if ("$(Get-DataProperty $Browser 'Engine')" -eq 'Firefox') { $source = "$(Get-DataProperty $Extension 'Source')" if ($source -eq 'enterprise-policy') { return 'Policy' } # Mozilla's own: a system add-on it updated, or one its experiments (Nimbus) put into # the profile. Observed 2026-10-14: the new tab page, in the profile, from Nimbus. if ($source -eq 'system-addon' -or $source -like 'nimbus*') { return 'BuiltIn' } $origin = $script:BrowserFirefoxOrigin["$location"] } else { if ([bool](Get-DataProperty $Extension 'ByDefault')) { return 'BuiltIn' } $number = ConvertTo-Number $location $origin = $null if ($null -ne $number) { $origin = $script:BrowserChromiumOrigin[[int]$number] } } if ($origin) { return $origin } 'Unknown' } function Get-BrowserOriginText { # How an extension came, in words. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Browser, [AllowNull()]$Extension) switch (Get-BrowserExtensionOrigin -Browser $Browser -Extension $Extension) { 'Store' { return (Get-Text 'Value.Browser.Origin.Store') } 'Policy' { return (Get-Text 'Value.Browser.Origin.Policy') } 'Developer' { return (Get-Text 'Value.Browser.Origin.Developer') } 'Program' { return (Get-Text 'Value.Browser.Origin.Program') } 'BuiltIn' { return (Get-Text 'Value.Browser.Origin.BuiltIn') } default { return (Get-Text 'Value.Browser.Origin.Unknown' "$(Get-DataProperty $Extension 'Location')") } } } function Get-BrowserChosenExtension { <# .SYNOPSIS The extensions of a profile that somebody chose: all but what the browser brings, and of Firefox's add-ons only extensions and themes. Pure. .DESCRIPTION Firefox lists its language packs and dictionaries among its add-ons. They change nothing a page does, and counted they would make a profile with a German dictionary a profile with an extension. An add-on that names no type is counted. #> [CmdletBinding()] param([AllowNull()]$Browser, [AllowNull()]$Row) $firefox = "$(Get-DataProperty $Browser 'Engine')" -eq 'Firefox' @((Get-DataCollection $Row 'Extensions') | Where-Object { $type = "$(Get-DataProperty $_ 'Type')".Trim() (Get-BrowserExtensionOrigin -Browser $Browser -Extension $_) -ne 'BuiltIn' -and -not ($firefox -and $type -and $type -notin 'extension', 'theme') }) } function Get-BrowserExtensionName { # What an extension is called in the Report: its name, or its id where it has none. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Extension) $name = "$(Get-DataProperty $Extension 'Name')".Trim() if ($name) { return $name } "$(Get-DataProperty $Extension 'Id')" } function New-BrowserExtensionFinding { <# .SYNOPSIS How many extensions somebody installed in each profile of a browser. .DESCRIPTION Built-in extensions are not counted: Edge brings a dozen and would always be "full". Any at all is INFO, so that "slow since last week" is held against the list; more than ExtensionWarnAbove in one profile is a WARN. #> [CmdletBinding()] param([AllowNull()]$Data, [AllowNull()]$Browser, [double]$WarnAbove) $name = "$(Get-DataProperty $Browser 'Browser')" $profiles = @((Get-DataCollection $Browser 'Profiles')) $most = 0 $items = @($profiles | ForEach-Object { if ("$(Get-DataProperty $_ 'Problem')") { return (Get-Text 'Value.Browser.ProfileItem' "$(Get-DataProperty $_ 'Profile')" (Get-Text 'Value.Browser.Unread' (Get-BrowserProblemText -Row $_))) } $chosen = @(Get-BrowserChosenExtension -Browser $Browser -Row $_) if ($chosen.Count -gt $most) { $most = $chosen.Count } if (-not $chosen.Count) { return (Get-Text 'Value.Browser.ProfileItem' "$(Get-DataProperty $_ 'Profile')" (Get-Text 'Value.Browser.ExtensionsNone')) } $said = @("$($chosen.Count)") $off = @($chosen | Where-Object { (Get-DataProperty $_ 'Enabled') -eq $false }).Count $policy = @($chosen | Where-Object { (Get-BrowserExtensionOrigin -Browser $Browser -Extension $_) -eq 'Policy' }).Count if ($off) { $said += Get-Text 'Value.Browser.ExtensionsOff' $off } if ($policy) { $said += Get-Text 'Value.Browser.ExtensionsByPolicy' $policy } Get-Text 'Value.Browser.ProfileItem' "$(Get-DataProperty $_ 'Profile')" ($said -join ', ') }) $check = Get-Text 'Check.Browser.Extensions' $name $value = $items -join '; ' $reference = @() if ($most) { $reference = @(New-Reference -Section 'Title.Browser.Extensions' -Argument $name) } # What a policy forces and blocks, so that a missing extension is explained by the # policy that blocks it. Counted, and the policies themselves are in their Section. $ruled = Get-BrowserExtensionPolicyCount -Data $Data -Browser $name $parts = @() if ($ruled.Forced) { $parts += Get-Text 'Value.Browser.ExtensionsForced' $ruled.Forced } if ($ruled.BlockedAll) { $parts += Get-Text 'Value.Browser.ExtensionsBlockedAll' } elseif ($ruled.Blocked) { $parts += Get-Text 'Value.Browser.ExtensionsBlocked' $ruled.Blocked } if ($parts.Count) { $value = '{0} | {1}' -f $value, (Get-Text 'Value.Browser.ExtensionPolicy' ($parts -join ', ')) $reference += New-Reference -Section 'Title.Browser.Policies' -Argument $name } # A profile whose list could not be read is a gap, and never reads as a profile with # nothing in it: INFO at least, with its Hint beside the others. $unread = [bool]@($profiles | Where-Object { "$(Get-DataProperty $_ 'Problem')" }).Count $hint = @() if ($most -gt $WarnAbove) { $hint += Get-Text 'Hint.Browser.ExtensionsMany' } elseif ($most) { $hint += Get-Text 'Hint.Browser.Extensions' } if ($unread) { $hint += Get-Text 'Hint.Browser.Unread' } if ($most -gt $WarnAbove) { return New-Finding -Category Browser -Check $check -Severity WARN -Value $value -Subject $name -Reference $reference ` -Meaning (Get-Text 'Meaning.Browser.ExtensionsMany') -Hint ($hint -join ' | ') } if ($most -or $unread) { return New-Finding -Category Browser -Check $check -Severity INFO -Value $value -Subject $name -Reference $reference -Hint ($hint -join ' | ') } New-Finding -Category Browser -Check $check -Severity OK -Value $value -Subject $name -Reference $reference } function New-BrowserUnusualExtensionFinding { <# .SYNOPSIS The extensions that came from no store and no policy: loaded in developer mode or put there by another program. Nothing where there are none. .DESCRIPTION A WARN: nobody checked them, and they are the first suspects for a browser that changed by itself - a search engine replaced, advertising on every page. #> [CmdletBinding()] param([AllowNull()]$Browser) $name = "$(Get-DataProperty $Browser 'Browser')" $items = @(foreach ($row in @((Get-DataCollection $Browser 'Profiles'))) { foreach ($one in @(Get-BrowserChosenExtension -Browser $Browser -Row $row)) { if ((Get-BrowserExtensionOrigin -Browser $Browser -Extension $one) -notin 'Developer', 'Program') { continue } Get-Text 'Value.Browser.UnusualItem' (Get-BrowserExtensionName -Extension $one) "$(Get-DataProperty $row 'Profile')" (Get-BrowserOriginText -Browser $Browser -Extension $one) } }) if (-not $items.Count) { return } New-Finding -Category Browser -Check (Get-Text 'Check.Browser.ExtensionsUnusual' $name) -Severity WARN -Subject $name -Value ($items -join '; ') ` -Meaning (Get-Text 'Meaning.Browser.ExtensionsUnusual') -Hint (Get-Text 'Hint.Browser.ExtensionsUnusual') ` -Reference @(New-Reference -Section 'Title.Browser.Extensions' -Argument $name) } function Get-BrowserProfileSize { <# .SYNOPSIS A profile's size as the Judge reads it: the bytes of the profile with its cache, the bytes of the cache, and how the measurement went. $null where it was not measured at all. Pure. .DESCRIPTION State is Complete; CutShort where a part was not counted to its end before the time ran out, and the numbers are a lower bound; or Unreadable where nothing of it could be counted. A cache folder that is not there is a cache of nothing. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Row) $parts = @((Get-DataCollection $Row 'Size')) if (-not $parts.Count) { return $null } $counted = @($parts | Where-Object { "$(Get-DataProperty $_ 'State')" -in 'Complete', 'CutShort' }) $sum = { param($List) [long](@($List | ForEach-Object { ConvertTo-Number (Get-DataProperty $_ 'Bytes') } | Where-Object { $null -ne $_ }) | Measure-Object -Sum).Sum } $profileParts = @($parts | Where-Object { "$(Get-DataProperty $_ 'Part')" -eq 'Profile' }) $state = 'Complete' if (@($parts | Where-Object { "$(Get-DataProperty $_ 'State')" -in 'CutShort', 'NotMeasured' }).Count) { $state = 'CutShort' } if (-not @($profileParts | Where-Object { "$(Get-DataProperty $_ 'State')" -in 'Complete', 'CutShort' }).Count -and $state -ne 'CutShort') { $state = 'Unreadable' } [pscustomobject]@{ State = $state Bytes = & $sum $counted Cache = & $sum @($counted | Where-Object { "$(Get-DataProperty $_ 'Part')" -eq 'Cache' }) } } function Get-BrowserSizeText { # A size in a cell or a sentence: at least, where the measurement was cut short. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Bytes, [AllowEmptyString()][string]$State) Format-DataSize -Bytes $Bytes -AtLeast:($State -eq 'CutShort') } function New-BrowserSizeFinding { <# .SYNOPSIS How large each profile of a browser is, with its cache. .DESCRIPTION Above ProfileWarnAboveGB a WARN: a large profile slows the browser at every start and fills a roaming User Profile. The Hint names the cache apart, because emptying the cache is the obvious step and does nothing for a profile large by its own data. A measurement cut short or not possible is INFO: a gap is no small profile. #> [CmdletBinding()] param([AllowNull()]$Browser, [double]$WarnAboveGB, [AllowNull()]$Seconds) $name = "$(Get-DataProperty $Browser 'Browser')" $measured = @(foreach ($row in @((Get-DataCollection $Browser 'Profiles'))) { $size = Get-BrowserProfileSize -Row $row if ($size) { [pscustomobject]@{ Profile = "$(Get-DataProperty $row 'Profile')"; Size = $size } } }) if (-not $measured.Count) { return } $items = @($measured | ForEach-Object { if ($_.Size.State -eq 'Unreadable') { return (Get-Text 'Value.Browser.ProfileItem' $_.Profile (Get-Text 'Value.Cell.Unreadable')) } $said = Get-Text 'Value.Browser.Size' (Get-BrowserSizeText -Bytes $_.Size.Bytes -State $_.Size.State) (Get-BrowserSizeText -Bytes $_.Size.Cache -State $_.Size.State) Get-Text 'Value.Browser.ProfileItem' $_.Profile $said }) $cut = @($measured | Where-Object { $_.Size.State -eq 'CutShort' }) if ($cut.Count) { $items += Get-Text 'Value.Browser.SizeCutShort' $Seconds } $check = Get-Text 'Check.Browser.Size' $name $value = $items -join '; ' $reference = @(New-Reference -Section 'Title.Browser.Profiles' -Argument $name) $large = @($measured | Where-Object { $_.Size.State -ne 'Unreadable' -and $_.Size.Bytes -gt ($WarnAboveGB * 1GB) } | Sort-Object { $_.Size.Bytes } -Descending) if ($large.Count) { return New-Finding -Category Browser -Check $check -Severity WARN -Value $value -Subject $name -Reference $reference ` -Meaning (Get-Text 'Meaning.Browser.SizeLarge') -Hint (Get-Text 'Hint.Browser.SizeLarge' (Get-BrowserSizeText -Bytes $large[0].Size.Cache -State $large[0].Size.State)) } if (@($measured | Where-Object { $_.Size.State -ne 'Complete' }).Count) { return New-Finding -Category Browser -Check $check -Severity INFO -Value $value -Subject $name -Reference $reference -Hint (Get-Text 'Hint.Browser.SizeCutShort') } New-Finding -Category Browser -Check $check -Severity OK -Value $value -Subject $name -Reference $reference } function Get-BrowserPolicyMeaning { # What a policy is for, in words, where Gutcheck knows it; '' where it does not. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Name) switch -Exact ($Name) { 'ProxyMode' { return (Get-Text 'Value.Browser.Policy.ProxyMode') } 'Proxy\Mode' { return (Get-Text 'Value.Browser.Policy.ProxyMode') } 'ProxyServer' { return (Get-Text 'Value.Browser.Policy.ProxyServer') } 'Proxy\HTTPProxy' { return (Get-Text 'Value.Browser.Policy.ProxyServer') } 'ProxyPacUrl' { return (Get-Text 'Value.Browser.Policy.ProxyPacUrl') } 'Proxy\AutoConfigURL' { return (Get-Text 'Value.Browser.Policy.ProxyPacUrl') } 'ProxyBypassList' { return (Get-Text 'Value.Browser.Policy.ProxyBypassList') } 'ProxySettings' { return (Get-Text 'Value.Browser.Policy.ProxySettings') } 'Proxy' { return (Get-Text 'Value.Browser.Policy.ProxySettings') } 'ExtensionInstallForcelist' { return (Get-Text 'Value.Browser.Policy.ExtensionInstallForcelist') } 'ExtensionInstallBlocklist' { return (Get-Text 'Value.Browser.Policy.ExtensionInstallBlocklist') } 'ExtensionInstallAllowlist' { return (Get-Text 'Value.Browser.Policy.ExtensionInstallAllowlist') } 'ExtensionSettings' { return (Get-Text 'Value.Browser.Policy.ExtensionSettings') } 'HomepageLocation' { return (Get-Text 'Value.Browser.Policy.HomepageLocation') } 'RestoreOnStartup' { return (Get-Text 'Value.Browser.Policy.RestoreOnStartup') } 'IncognitoModeAvailability' { return (Get-Text 'Value.Browser.Policy.IncognitoModeAvailability') } 'InPrivateModeAvailability' { return (Get-Text 'Value.Browser.Policy.InPrivateModeAvailability') } 'BrowserSignin' { return (Get-Text 'Value.Browser.Policy.BrowserSignin') } 'SyncDisabled' { return (Get-Text 'Value.Browser.Policy.SyncDisabled') } 'PasswordManagerEnabled' { return (Get-Text 'Value.Browser.Policy.PasswordManagerEnabled') } 'SmartScreenEnabled' { return (Get-Text 'Value.Browser.Policy.SmartScreenEnabled') } 'DeveloperToolsAvailability' { return (Get-Text 'Value.Browser.Policy.DeveloperToolsAvailability') } 'DisableAppUpdate' { return (Get-Text 'Value.Browser.Policy.DisableAppUpdate') } } '' } function Get-BrowserProxyMode { # A proxy mode as Chromium or Firefox writes it, as one of Direct, System, Detect, # Script, Server; '' for a word neither writes. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Value) switch -Exact ("$Value".Trim()) { 'direct' { return 'Direct' } 'none' { return 'Direct' } 'system' { return 'System' } 'auto_detect' { return 'Detect' } 'autoDetect' { return 'Detect' } 'pac_script' { return 'Script' } 'autoConfig' { return 'Script' } 'fixed_servers' { return 'Server' } 'manual' { return 'Server' } } '' } function Get-BrowserProxyModeText { # A proxy mode in words. Pure. [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Mode) switch ($Mode) { 'Direct' { return (Get-Text 'Value.Browser.ProxyMode.Direct') } 'System' { return (Get-Text 'Value.Browser.ProxyMode.System') } 'Detect' { return (Get-Text 'Value.Browser.ProxyMode.Detect') } 'Script' { return (Get-Text 'Value.Browser.ProxyMode.Script') } 'Server' { return (Get-Text 'Value.Browser.ProxyMode.Server') } } '' } function Get-BrowserPolicyValueText { <# .SYNOPSIS A policy's value as the Report shows it: a list one entry after the other, a value Gutcheck knows with what it means behind it, anything else as it is. Pure. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Policy) $name = "$(Get-DataProperty $Policy 'Name')" $value = Get-DataProperty $Policy 'Value' if ($value -is [array]) { return (@($value | ForEach-Object { "$_" }) -join '; ') } $text = "$value".Trim() $meaning = '' switch -Exact ($name) { { $_ -in 'ProxyMode', 'Proxy\Mode' } { $meaning = Get-BrowserProxyModeText -Mode (Get-BrowserProxyMode -Value $text) } 'RestoreOnStartup' { switch ($text) { '1' { $meaning = Get-Text 'Value.Browser.Policy.RestoreOnStartup.1' } '4' { $meaning = Get-Text 'Value.Browser.Policy.RestoreOnStartup.4' } '5' { $meaning = Get-Text 'Value.Browser.Policy.RestoreOnStartup.5' } '6' { $meaning = Get-Text 'Value.Browser.Policy.RestoreOnStartup.6' } } } { $_ -in 'IncognitoModeAvailability', 'InPrivateModeAvailability' } { switch ($text) { '0' { $meaning = Get-Text 'Value.Browser.Policy.Allowed' } '1' { $meaning = Get-Text 'Value.Browser.Policy.Blocked' } '2' { $meaning = Get-Text 'Value.Browser.Policy.Forced' } } } 'BrowserSignin' { switch ($text) { '0' { $meaning = Get-Text 'Value.Browser.Policy.Blocked' } '1' { $meaning = Get-Text 'Value.Browser.Policy.Allowed' } '2' { $meaning = Get-Text 'Value.Browser.Policy.Forced' } } } 'DeveloperToolsAvailability' { switch ($text) { '0' { $meaning = Get-Text 'Value.Browser.Policy.DeveloperTools.0' } '1' { $meaning = Get-Text 'Value.Browser.Policy.Allowed' } '2' { $meaning = Get-Text 'Value.Browser.Policy.Blocked' } } } { $_ -in 'SyncDisabled', 'PasswordManagerEnabled', 'SmartScreenEnabled', 'DisableAppUpdate' } { if ($text -in '1', 'true') { $meaning = Get-Text 'Value.Browser.Policy.Yes' } if ($text -in '0', 'false') { $meaning = Get-Text 'Value.Browser.Policy.No' } } } Format-CodeWithMeaning -Code $text -Meaning $meaning } function Get-BrowserPolicyRow { # The policies of one browser, the machine's before the User's: the browser takes the # machine's where both set one. Pure. [CmdletBinding()] param([AllowNull()]$Data, [AllowEmptyString()][string]$Browser) $mine = @((Get-DataCollection $Data 'Policies') | Where-Object { "$(Get-DataProperty $_ 'Browser')" -eq $Browser }) @(@($mine | Where-Object { "$(Get-DataProperty $_ 'Scope')" -eq 'Machine' }) + @($mine | Where-Object { "$(Get-DataProperty $_ 'Scope')" -ne 'Machine' })) } function Get-BrowserPolicyValue { # The value of one policy of a browser as the browser takes it, $null where none is set. Pure. [CmdletBinding()] param([AllowNull()]$Data, [AllowEmptyString()][string]$Browser, [Parameter(Mandatory)][string]$Name) $row = @(Get-BrowserPolicyRow -Data $Data -Browser $Browser | Where-Object { "$(Get-DataProperty $_ 'Name')" -eq $Name })[0] if ($null -eq $row) { return $null } Get-DataProperty $row 'Value' } function ConvertFrom-BrowserPolicyJson { # A policy that holds JSON as an object, $null where it holds none. Pure. [CmdletBinding()] param([AllowNull()]$Value) if ($null -eq $Value -or $Value -is [array] -or -not "$Value".Trim()) { return $null } try { "$Value" | ConvertFrom-Json -ErrorAction Stop } catch { $null } } function Get-BrowserPolicyProxy { <# .SYNOPSIS The proxy a policy makes a browser use: Mode (Direct, System, Detect, Script or Server), Server and Script. $null where no policy sets one. Pure. .DESCRIPTION Chromium takes "ProxySettings" before the single policies; Firefox has one policy, "Proxy", as JSON in its policy file and as a key with values in the registry. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [AllowNull()]$Browser) $name = "$(Get-DataProperty $Browser 'Browser')" if ("$(Get-DataProperty $Browser 'Engine')" -eq 'Firefox') { $json = ConvertFrom-BrowserPolicyJson (Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'Proxy') $mode = "$(Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'Proxy\Mode')" $server = "$(Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'Proxy\HTTPProxy')" $script = "$(Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'Proxy\AutoConfigURL')" if ($json) { $mode = "$(Get-DataProperty $json 'Mode')"; $server = "$(Get-DataProperty $json 'HTTPProxy')"; $script = "$(Get-DataProperty $json 'AutoConfigURL')" } } else { $json = ConvertFrom-BrowserPolicyJson (Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'ProxySettings') $mode = "$(Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'ProxyMode')" $server = "$(Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'ProxyServer')" $script = "$(Get-BrowserPolicyValue -Data $Data -Browser $name -Name 'ProxyPacUrl')" if ($json) { $mode = "$(Get-DataProperty $json 'ProxyMode')"; $server = "$(Get-DataProperty $json 'ProxyServer')"; $script = "$(Get-DataProperty $json 'ProxyPacUrl')" } } $said = Get-BrowserProxyMode -Value $mode if (-not $said) { return $null } [pscustomobject]@{ Mode = $said; Server = $server.Trim(); Script = $script.Trim() } } function Get-BrowserPolicyProxyText { # The proxy a policy sets, in words. Pure. [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)]$Proxy) switch ($Proxy.Mode) { 'Server' { return (Get-Text 'Value.Browser.ProxyPolicy.Server' $Proxy.Server) } 'Script' { return (Get-Text 'Value.Browser.ProxyPolicy.Script' $Proxy.Script) } } Get-BrowserProxyModeText -Mode $Proxy.Mode } function Test-BrowserProxySame { <# .SYNOPSIS Whether the proxy a policy sets is the one the User has in Windows. Pure. .DESCRIPTION "System" is the User's by definition. A server is the same where the User has it switched on; a script where the User has the same one. Direct and automatic detection are the same where the User has neither a server nor a script. #> [CmdletBinding()] [OutputType([bool])] param([Parameter(Mandatory)]$Proxy, [AllowNull()]$UserProxy) $userServer = "$(Get-UserProxyServer $UserProxy)".Trim() $userScript = "$(Get-DataProperty $UserProxy 'AutoConfigURL')".Trim() switch ($Proxy.Mode) { 'System' { return $true } 'Server' { return [bool]($userServer -and $userServer -eq $Proxy.Server) } 'Script' { return [bool]($userScript -and $userScript -eq $Proxy.Script) } } -not $userServer -and -not $userScript } function New-BrowserProxyFinding { <# .SYNOPSIS The proxy a policy forces on a browser beside the User's and WinHTTP's, where a policy sets one. Nothing where none does. .DESCRIPTION INFO where it differs from the User's: no fault, but the answer to "the internet works in one browser and not in the other program". Where the Network Check did not run, the User's proxy is not known here, and that is said; it is not read a second time. #> [CmdletBinding()] param([AllowNull()]$Data, [AllowNull()]$Browser) $proxy = Get-BrowserPolicyProxy -Data $Data -Browser $Browser if (-not $proxy) { return } $name = "$(Get-DataProperty $Browser 'Browser')" $check = Get-Text 'Check.Browser.ProxyPolicy' $name $policy = Get-BrowserPolicyProxyText -Proxy $proxy $reference = @(New-Reference -Section 'Title.Browser.Policies' -Argument $name) if (-not [bool](Get-DataProperty $Data 'NetworkObserved')) { return New-Finding -Category Browser -Check $check -Severity INFO -Subject $name -Reference $reference ` -Value ('{0} | {1}' -f (Get-Text 'Value.Browser.ProxyOnly' $policy), (Get-Text 'Value.Browser.ProxyNotCompared')) -Hint (Get-Text 'Hint.Browser.ProxyNotCompared') } $user = Get-DataProperty $Data 'UserProxy' $winHttp = @(Get-WinHttpProxyView -Data $Data)[0] $value = Get-Text 'Value.Browser.ProxyCompare' $policy (Get-UserProxyDescription $user) (Get-WinHttpProxyDescription $winHttp.Parsed) $reference += New-Reference -Section 'Title.Network.ProxySettings' if ($null -eq $user -or (Test-BrowserProxySame -Proxy $proxy -UserProxy $user)) { if ($null -eq $user) { return New-Finding -Category Browser -Check $check -Severity INFO -Subject $name -Reference $reference -Value $value -Hint (Get-Text 'Hint.Browser.ProxyNotCompared') } return New-Finding -Category Browser -Check $check -Severity OK -Subject $name -Reference $reference -Value $value } New-Finding -Category Browser -Check $check -Severity INFO -Subject $name -Reference $reference -Value $value ` -Meaning (Get-Text 'Meaning.Browser.ProxyDiffers') -Hint (Get-Text 'Hint.Browser.ProxyDiffers') } function Get-BrowserExtensionPolicyCount { <# .SYNOPSIS How many extensions a policy forces on a browser and how many it blocks, and whether it blocks all it does not name. Pure. .DESCRIPTION Chromium has a list of each, and "ExtensionSettings" beside them; Firefox has only "ExtensionSettings". An installation mode of force_installed or normal_installed is forced, blocked is blocked; "*" blocked blocks every one not named. Firefox's language packs and dictionaries are not counted: the extension list leaves them out, and a count that included them would say ten were forced beside a list of none. In a policy only their ids show what they are, by the names Mozilla gives them. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [AllowEmptyString()][string]$Browser) $forced = New-Object System.Collections.Generic.HashSet[string] $blocked = New-Object System.Collections.Generic.HashSet[string] $all = $false foreach ($row in @(Get-BrowserPolicyRow -Data $Data -Browser $Browser)) { $value = Get-DataProperty $row 'Value' switch -Exact ("$(Get-DataProperty $row 'Name')") { 'ExtensionInstallForcelist' { foreach ($one in @($value)) { $id = ("$one" -split ';')[0].Trim(); if ($id) { $null = $forced.Add($id) } } } 'ExtensionInstallBlocklist' { foreach ($one in @($value)) { $id = "$one".Trim(); if ($id -eq '*') { $all = $true } elseif ($id) { $null = $blocked.Add($id) } } } 'ExtensionSettings' { $json = ConvertFrom-BrowserPolicyJson $value if (-not $json) { break } foreach ($one in $json.PSObject.Properties) { if ($one.Name -match '^langpack-.*@firefox\.mozilla\.org$|@dictionaries\.addons\.mozilla\.org$') { continue } $mode = "$(Get-DataProperty $one.Value 'installation_mode')" if ($one.Name -eq '*') { if ($mode -eq 'blocked') { $all = $true }; continue } if ($mode -in 'force_installed', 'normal_installed') { $null = $forced.Add($one.Name) } elseif ($mode -eq 'blocked') { $null = $blocked.Add($one.Name) } } } } } [pscustomobject]@{ Forced = $forced.Count; Blocked = $blocked.Count; BlockedAll = $all } } function New-BrowserPolicyFinding { <# .SYNOPSIS How many policies are set for each browser, for the machine and for the User, or why they could not be read. #> [CmdletBinding()] param([AllowNull()]$Data, [AllowNull()][AllowEmptyCollection()]$Browser) if ($null -eq (Get-DataProperty $Data 'PoliciesRead')) { return } $policies = @((Get-DataCollection $Data 'Policies')) $names = @(@($Browser | ForEach-Object { "$(Get-DataProperty $_ 'Browser')" }) + @($policies | ForEach-Object { "$(Get-DataProperty $_ 'Browser')" }) | Select-Object -Unique) # One for each browser, about it: it stands under the browser with the rest of it. foreach ($name in $names) { $check = Get-Text 'Check.Browser.Policies' $name if (-not [bool](Get-DataProperty $Data 'PoliciesRead')) { New-Finding -Category Browser -Check $check -Severity INFO -Subject $name ` -Value (Get-Text 'Value.Browser.Unread' "$(Get-DataProperty $Data 'PoliciesReason')") -Hint (Get-Text 'Hint.Browser.PoliciesUnread') continue } $mine = @($policies | Where-Object { "$(Get-DataProperty $_ 'Browser')" -eq $name }) if (-not $mine.Count) { New-Finding -Category Browser -Check $check -Severity OK -Subject $name -Value (Get-Text 'Value.Browser.PoliciesNone') continue } $machine = @($mine | Where-Object { "$(Get-DataProperty $_ 'Scope')" -eq 'Machine' }).Count New-Finding -Category Browser -Check $check -Severity OK -Subject $name ` -Value (Get-Text 'Value.Browser.PoliciesOf' $machine ($mine.Count - $machine)) ` -Reference @(New-Reference -Section 'Title.Browser.Policies' -Argument $name) } } function Select-BrowserLoggedCrash { # The crashes of one program among those the Stability Check read, the newest first, # each with its time as a time. Pure. [CmdletBinding()] param([AllowNull()]$Data, [AllowEmptyString()][string]$Program) @((Get-DataCollection $Data 'ObservedCrashes') | Where-Object { "$(Get-DataProperty $_ 'App')".Trim() -eq $Program } | ForEach-Object { $time = ConvertTo-DataTime (Get-DataProperty $_ 'Time') if ($time) { [pscustomobject]@{ Time = $time } } } | Sort-Object Time -Descending) } function New-BrowserWebView2Finding { <# .SYNOPSIS Whether the WebView2 runtime is installed, and which version. .DESCRIPTION A WARN where it is missing: new Outlook, Teams and the web add-ins of Office are drawn in it, and without it their windows stay white. Nothing where the Gatherer did not look. #> [CmdletBinding()] param([AllowNull()]$Data) $runtime = Get-DataProperty $Data 'WebView2' if ($null -eq $runtime) { return } $check = Get-Text 'Check.Browser.WebView2' $usable = { param($Version) $v = "$Version".Trim(); [bool]($v -and $v -ne '0.0.0.0') } $machine = "$(Get-DataProperty $runtime 'Machine')".Trim() $user = "$(Get-DataProperty $runtime 'User')".Trim() if (& $usable $machine) { return New-Finding -Category Browser -Check $check -Severity OK -Subject 'WebView2' -Value (Get-Text 'Value.Browser.WebView2.Machine' $machine) } if (& $usable $user) { return New-Finding -Category Browser -Check $check -Severity OK -Subject 'WebView2' -Value (Get-Text 'Value.Browser.WebView2.User' $user) } New-Finding -Category Browser -Check $check -Severity WARN -Subject 'WebView2' -Value (Get-Text 'Value.Browser.WebView2.Missing') ` -Meaning (Get-Text 'Meaning.Browser.WebView2Missing') -Hint (Get-Text 'Hint.Browser.WebView2Missing') } function New-BrowserWebView2CrashFinding { <# .SYNOPSIS How often WebView2 (msedgewebview2.exe) crashed in the period, from the crashes the Stability Check read, with the newest. .DESCRIPTION Counted apart from Edge and Outlook, so that a white window is not laid at their door. A few are INFO, more than WebView2CrashWarnAbove a WARN; both refer to the Stability Check's table of the crashes, where the exception and the module are. Where the Stability Check did not run, that is said instead of "none". #> [CmdletBinding()] param([AllowNull()]$Data, [double]$WarnAbove) $observed = Get-DataProperty $Data 'StabilityObserved' if ($null -eq $observed) { return } $check = Get-Text 'Check.Browser.WebView2Crashes' if (-not [bool]$observed) { return New-Finding -Category Browser -Check $check -Severity INFO -Subject 'WebView2' ` -Value (Get-Text 'Value.Browser.WebView2CrashesNotCounted') -Hint (Get-Text 'Hint.Browser.WebView2CrashesNotCounted') } $days = Get-DataProperty $Data 'CrashWindowDays' $crashes = @(Select-BrowserLoggedCrash -Data $Data -Program 'msedgewebview2.exe') if (-not $crashes.Count) { return New-Finding -Category Browser -Check $check -Severity OK -Subject 'WebView2' -Value (Get-Text 'Value.Browser.WebView2CrashesNone' $days) } $value = Get-Text 'Value.Browser.WebView2Crashes' $crashes.Count $days $crashes[0].Time $reference = @(New-Reference -Section 'Title.Stability.ApplicationCrashesByApp') $severity = 'INFO' $hint = Get-Text 'Hint.Browser.WebView2Crashes' if ($crashes.Count -gt $WarnAbove) { $severity = 'WARN'; $hint = Get-Text 'Hint.Browser.WebView2CrashesMany' } New-Finding -Category Browser -Check $check -Severity $severity -Subject 'WebView2' -Value $value -LastSeen $crashes[0].Time ` -Reference $reference -Meaning (Get-Text 'Meaning.Browser.WebView2Crashes') -Hint $hint } function ConvertTo-BrowserFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) # The period the Stability Check reads, so that the crash reports and the crashes # Windows logged are counted over the same days. Read with Get-Parameter and a literal # name: that is what the published reference finds. # Where the Stability Check ran, the period it read; where it did not, the one it # would have read. $days = ConvertTo-Number (Get-Parameter $Parameters 'Days' $script:StabilityDefaultDays) if ($null -eq $days -or $days -le 0) { $days = $script:StabilityDefaultDays } $stabilityDays = ConvertTo-Number (Get-DataProperty $Data 'CrashWindowDays') if ([bool](Get-DataProperty $Data 'StabilityObserved') -and $null -ne $stabilityDays -and $stabilityDays -gt 0) { $days = $stabilityDays } # More than three crash reports in the period: a rule of thumb, no limit of a browser. # A browser that crashed once a week is unlucky; one that crashes every few days has # something wrong with it. $warnAbove = ConvertTo-Number (Get-Parameter $Parameters 'CrashReportWarnAbove' 3) if ($null -eq $warnAbove) { $warnAbove = 3 } # More than fifteen extensions somebody chose, in one profile: a rule of thumb. Each # runs on the pages it may read, and a browser loaded with them is slow for that. $extensionsAbove = ConvertTo-Number (Get-Parameter $Parameters 'ExtensionWarnAbove' 15) if ($null -eq $extensionsAbove) { $extensionsAbove = 15 } # More than 2 GB in one profile: a rule of thumb. A profile that size is slow to open # and, in a roaming User Profile, slow to sign in with. $sizeAboveGB = ConvertTo-Number (Get-Parameter $Parameters 'ProfileWarnAboveGB' 2) if ($null -eq $sizeAboveGB) { $sizeAboveGB = 2 } # More than three crashes of WebView2 in the period: the same rule of thumb as for the # crash reports of a browser. $webViewAbove = ConvertTo-Number (Get-Parameter $Parameters 'WebView2CrashWarnAbove' 3) if ($null -eq $webViewAbove) { $webViewAbove = 3 } $asOf = Get-DataProperty $Data 'GatheredAt' $seen = @((Get-DataCollection $Data 'Browsers') | Where-Object { [bool](Get-DataProperty $_ 'Installed') -or @((Get-DataCollection $_ 'Profiles')).Count }) if (-not $seen.Count) { # About no one browser: it stands with all the applications, under no one of them. New-Finding -Category Apps -Check (Get-Text 'Check.Browser.None') -Severity INFO ` -Value (Get-Text 'Value.Browser.None') -Hint (Get-Text 'Hint.Browser.None') } foreach ($browser in $seen) { $name = "$(Get-DataProperty $browser 'Browser')" $own = @( New-BrowserProxyFinding -Data $Data -Browser $browser if (-not @((Get-DataCollection $browser 'Profiles')).Count) { # Installed and never opened by this User: no gap in the reading, and nothing to do. New-Finding -Category Browser -Check (Get-Text 'Check.Browser.Unused' $name) -Severity OK -Subject $name -Value (Get-Text 'Value.Browser.Unused') } else { if ("$(Get-DataProperty $browser 'Engine')" -eq 'Chromium') { New-BrowserClosedFinding -Data $Data -Browser $browser } New-BrowserCrashReportFinding -Browser $browser -AsOf $asOf -Days $days -WarnAbove $warnAbove New-BrowserExtensionFinding -Data $Data -Browser $browser -WarnAbove $extensionsAbove New-BrowserUnusualExtensionFinding -Browser $browser New-BrowserSizeFinding -Browser $browser -WarnAboveGB $sizeAboveGB -Seconds (Get-DataProperty $Data 'MeasureSeconds') }) # Under the browser, where its App Check stands: a Technician looks for Edge in one # place. The Run keeps an App the Judge set (Set-CheckOrigin). foreach ($finding in $own) { $finding.App = $name; $finding } } # The policies are each browser's, and WebView2 is a runtime of its own that new # Outlook and Teams draw in: each under its own name, as an App Check would stand. $machine = @( if ($seen.Count) { New-BrowserPolicyFinding -Data $Data -Browser $seen } New-BrowserWebView2Finding -Data $Data New-BrowserWebView2CrashFinding -Data $Data -WarnAbove $webViewAbove) foreach ($finding in $machine) { $finding.App = "$($finding.Subject)"; $finding } } function ConvertTo-BrowserSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) foreach ($browser in @((Get-DataCollection $Data 'Browsers'))) { $profiles = @((Get-DataCollection $browser 'Profiles')) if (-not $profiles.Count) { continue } $name = "$(Get-DataProperty $browser 'Browser')" # Both tables of a browser stand under it, beside its App Check's evidence. $profileSection = New-Section -Title (Get-Text 'Title.Browser.Profiles' $name) -Help (Get-Text 'Help.Browser.Profiles') -Row @( $profiles | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Browser.Profile')] = "$(Get-DataProperty $_ 'Profile')" $row[(Get-Text 'Column.Browser.LastExit')] = Get-BrowserExitText -Browser $browser -Row $_ $row[(Get-Text 'Column.Browser.Extensions')] = $(if ("$(Get-DataProperty $_ 'Problem')") { '' } else { "$(@(Get-BrowserChosenExtension -Browser $browser -Row $_).Count)" }) $size = Get-BrowserProfileSize -Row $_ $row[(Get-Text 'Column.Browser.Size')] = $(if (-not $size) { '' } elseif ($size.State -eq 'Unreadable') { Get-Text 'Value.Cell.Unreadable' } else { Get-BrowserSizeText -Bytes $size.Bytes -State $size.State }) $row[(Get-Text 'Column.Browser.Cache')] = $(if (-not $size -or $size.State -eq 'Unreadable') { '' } else { Get-BrowserSizeText -Bytes $size.Cache -State $size.State }) $row[(Get-Text 'Column.Note')] = Get-BrowserProblemText -Row $_ [pscustomobject]$row } ) if ($profileSection) { $profileSection.App = $name; $profileSection } # The extensions somebody chose, a row each, in a table of their own: thirty rows # of them would bury the three rows of the profiles. $extensions = @(foreach ($one in $profiles) { foreach ($extension in @(Get-BrowserChosenExtension -Browser $browser -Row $one)) { $row = [ordered]@{} $row[(Get-Text 'Column.Browser.Profile')] = "$(Get-DataProperty $one 'Profile')" $row[(Get-Text 'Column.Browser.Extension')] = Get-BrowserExtensionName -Extension $extension $row[(Get-Text 'Column.Browser.Version')] = "$(Get-DataProperty $extension 'Version')" $row[(Get-Text 'Column.Browser.Enabled')] = Get-TriStateText (Get-DataProperty $extension 'Enabled') $row[(Get-Text 'Column.Browser.Origin')] = Get-BrowserOriginText -Browser $browser -Extension $extension $row[(Get-Text 'Column.Browser.Id')] = "$(Get-DataProperty $extension 'Id')" [pscustomobject]$row } }) $extensionSection = New-Section -Title (Get-Text 'Title.Browser.Extensions' $name) -Help (Get-Text 'Help.Browser.Extensions') -Row $extensions if ($extensionSection) { $extensionSection.App = $name; $extensionSection } } # The policies of each browser, as they were read, under that browser. $policies = @((Get-DataCollection $Data 'Policies')) foreach ($name in @($policies | ForEach-Object { "$(Get-DataProperty $_ 'Browser')" } | Select-Object -Unique)) { $section = New-Section -Title (Get-Text 'Title.Browser.Policies' $name) -Help (Get-Text 'Help.Browser.Policies') -Row @( $policies | Where-Object { "$(Get-DataProperty $_ 'Browser')" -eq $name } | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Browser.Scope')] = $(if ("$(Get-DataProperty $_ 'Scope')" -eq 'Machine') { Get-Text 'Value.Browser.Scope.Machine' } else { Get-Text 'Value.Browser.Scope.User' }) $row[(Get-Text 'Column.Browser.Policy')] = "$(Get-DataProperty $_ 'Name')" $row[(Get-Text 'Column.Browser.PolicyMeaning')] = Get-BrowserPolicyMeaning -Name "$(Get-DataProperty $_ 'Name')" $row[(Get-Text 'Column.Browser.Value')] = Get-BrowserPolicyValueText -Policy $_ [pscustomobject]$row } ) if ($section) { $section.App = $name; $section } } } |