#Requires -Version 5.1 # PIM Manager 3.0.70 -- Deploy-PimActivatorClient.ps1, standalone (built by tools/setup/Build-PimSupportScripts.ps1; helpers inlined: ..\setup\_pimscriptdoc.ps1, _pimactivatorauth.ps1, ..\setup\_pimloopbackanswer.ps1, _pimactivatorhybridpolicy.ps1) <# .SYNOPSIS Installs the PIM Activator browser extension on this machine (Edge, Chrome or both) through local Chromium enterprise policy in the registry -- force-install, install source, extension settings and the tenant catalog -- for servers and machines without Intune; -Uninstall removes only what it wrote. .DESCRIPTION Documentation (purpose, who may run it, every change and permission, -WhatIf, undo): https://invardia.com/docs/pim/scripts/Deploy-PimActivatorClient/ -WhatIf reads the machine's current policies (and signs in only when the tenant catalog is auto-discovered) and prints every registry value it would set or remove; it writes nothing. The extension itself is downloaded by the BROWSER, not by this script: the forcelist names the update manifest (-UpdateUrl) and the browser installs and updates the signed CRX from it (Chromium accepts a CRX only when its signing key matches the extension id). Designed for unattended Intune / Group Policy / Configuration Manager rollout. Writes the Chromium policies that tell the browser to install + auto-update the extension from the given -UpdateUrl on next launch (ExtensionInstallForcelist, ExtensionInstallSources, ExtensionSettings), plus the tenant catalog the popup reads from chrome.storage.managed. Per-machine (-Scope Machine, default, HKLM, requires admin) or per-user (-Scope User, HKCU, no admin). Both browsers read the SAME key names under different roots: Edge : SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallForcelist Chrome : SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist The tenant catalog (tenant id + client id per tenant) goes to \3rdparty\extensions\\policy\tenantCatalog, which the popup's "Use centrally deployed" option reads. Users can instead import a catalog or type one tenant in the popup's setup wizard. Re-runnable. Safe under SYSTEM. Exit code 0 on success; 1 when the tenant catalog was asked for but could not be written (the policies are still written). .PARAMETER ExtensionId 32-char Chromium extension id (lowercase a-p). Required. .PARAMETER UpdateUrl The update.xml URL the extension auto-updates from. For the PIM4EntraPS-hosted CRX this is https://knudsenmorten.github.io/PIM4EntraPS/updates.xml .PARAMETER Scope 'Machine' (default, HKLM, requires admin, force-installs for every user on the box) or 'User' (HKCU, no admin required, current-user only, no Intune conflict). On Intune-managed devices the HKLM ExtensionInstallForcelist value will be overwritten by Intune's policy on next sync -- use Intune's own Edge management service policy instead (see README.md: Prerequisites) and the Intune Remediation in intune-remediation\. .PARAMETER Browser 'Edge', 'Chrome', or 'Both' (default). .PARAMETER Uninstall Remove ONLY what this script wrote for this extension id: our forcelist / allowlist rows, our install-source row (kept while another forcelist row still installs from that host), our ExtensionSettings entry and our 3rdparty policy tree. The org's own rows and ExtensionSettings entries are left in place. .PARAMETER ClientId Optional. The PIM Activator app registration's Application (client) id for the tenant catalog. When omitted, the app is resolved by EXACT display name (-AppDisplayName) and must be unique -- never "the first app whose name starts with PIM Activator". .PARAMETER AppDisplayName Exact display name used to find the app registration when -ClientId is not given. Default 'PIM Activator' (what Deploy-PimActivatorBackend.ps1 creates). .PARAMETER TenantId With -ClientId: write the tenant catalog from these values (no sign-in, no file). .PARAMETER AutoActivateMaxGroups Per-device cap on auto-activation: 0 = off, N = at most N groups, -1 = remove the cap. Not passed = left alone. Written for both PIM Activator extensions (production and test), the same as the Intune Remediation. .PARAMETER BulkActivateConfirmThreshold Per-device "click again to confirm" threshold for bulk activation: 1-100, -1 = remove it. Not passed = left alone. Both extensions, as above. A tenant's own bulkActivateConfirmThreshold in the catalog wins over it. .PARAMETER ActivationEndTime Per-device end-of-day time 'HH:MM' (local time): every activation ends no later than this today; one started after it gets its normal duration. 'none' = remove it. Not passed = left alone. Both extensions, as above. .PARAMETER Prefix With -TenantId and -ClientId: list only groups whose names start with this text (-EntraPrefix: Entra roles, -AzurePrefix: Azure roles). The same settings as the Intune Remediation. .EXAMPLE # Server without Intune, the same settings as the Intune Remediation (the command the PIM Manager page shows): .\Deploy-PimActivatorClient.ps1 -Scope Machine -Browser Both -Channel Released -TenantId -ClientId ` -TenantName 'Contoso' -AutoActivateMaxGroups 3 -BulkActivateConfirmThreshold 5 .EXAMPLE # Preview: every registry value it would set -- writes nothing: .\Deploy-PimActivatorClient.ps1 -Scope Machine -Browser Both -TenantId -ClientId -WhatIf .EXAMPLE # Default per-machine install (HKLM, requires admin, applies to every # user on this box): .\Deploy-PimActivatorClient.ps1 # On next Edge / Chrome launch the extension installs for every user. # Each user then opens the popup, picks the centrally deployed tenant, # signs in, and starts activating PIM eligibilities. .EXAMPLE # Current-user install (HKCU, no admin needed, won't conflict with # Intune-pushed policy): .\Deploy-PimActivatorClient.ps1 -Scope User .EXAMPLE # Edge only: .\Deploy-PimActivatorClient.ps1 ` -ExtensionId 'eheocihmlppcophaeakmdenhgcookkab' ` -UpdateUrl 'https://knudsenmorten.github.io/PIM4EntraPS/updates.xml' ` -Browser Edge .EXAMPLE # Force the org's activation defaults (justification + duration) for every # tenant entry written -- overrides the auto-discover fallback ('Change in # infrastructure' / 8h) or any value carried in -CatalogJsonPath: .\Deploy-PimActivatorClient.ps1 ` -DefaultJustification 'Approved change / incident work' ` -DefaultDurationHours 4 .EXAMPLE # Server: cap auto-activation at 3 groups on THIS machine (both extensions), leaving the rest as is. # 0 turns auto-activation off; -1 removes the cap again. .\Deploy-PimActivatorClient.ps1 -Channel Test -AutoActivateMaxGroups 3 .EXAMPLE # Uninstall (removes only OUR policy rows / entries; the extension # self-removes on next launch): .\Deploy-PimActivatorClient.ps1 ` -ExtensionId 'eheocihmlppcophaeakmdenhgcookkab' ` -Uninstall .EXAMPLE # Intune managed-policy equivalent (per-user, no script needed): # Microsoft Edge -> Configuration profile -> Settings catalog # -> Extensions -> Configure which extensions are installed silently # -> Add: eheocihmlppcophaeakmdenhgcookkab;https://knudsenmorten.github.io/PIM4EntraPS/updates.xml .NOTES Requirements: PowerShell: Windows PowerShell 5.1 (powershell.exe) or PowerShell 7 on Windows -- one of PIM's three exceptions to PowerShell 7. Why: Group Policy startup scripts and Configuration Manager run it in Windows PowerShell on machines that have no PowerShell 7. Role: local Administrator (elevated PowerShell) for -Scope Machine, the default; none for -Scope User. No Microsoft Entra role: only the tenant lookup (without -TenantId and -ClientId) signs in, to read the organisation and the PIM Activator app. Hosts: none when -TenantId and -ClientId are given; otherwise login.microsoftonline.com, graph.microsoft.com. The browser, not this script, then fetches the extension from its update address (-UpdateUrl). PERMISSIONS AND CHANGES (from Deploy-PimActivatorClient.ps1 manifest, PIM Manager 3.0.70; full page: https://invardia.com/docs/pim/scripts/Deploy-PimActivatorClient/) The person running it needs: - Local Administrator (elevated PowerShell) [other, this machine; only for -Scope Machine (HKLM, the default)] -- machine policy lives under HKLM. -Scope User (HKCU, the current user only) needs no elevation. - Delegated sign-in: Organization.Read.All, Application.Read.All (read only) [other, Microsoft Graph, for the first-party app Microsoft Graph Command Line Tools] -- ONLY when neither -TenantId + -ClientId nor -CatalogJsonPath is given: the tenant and the PIM Activator app are looked up live. The command PIM Manager shows passes both, so it signs in to nothing. What it creates, changes or grants: - update \SOFTWARE\Policies\Microsoft\Edge and \Google\Chrome \ExtensionInstallForcelist: one row ';' in the slot already holding our id, else the lowest free slot; other rows carrying our id are removed (this machine's registry) -- the browser installs and keeps the extension updated Undo: Deploy-PimActivatorClient.ps1 -Uninstall (removes only our rows) - update ...\ExtensionInstallSources: one row with the update host pattern (https:///*) (this machine's registry) -- allows the install where a hardened baseline restricts install sources Undo: -Uninstall (kept while another forcelist row still installs from that host) - update ...\ExtensionSettings: our entry (force_installed, runtime_allowed_hosts) merged into the organisation's dictionary -- their entries are kept (this machine's registry) -- an extension update can never be silently disabled as a permission expansion Undo: -Uninstall (takes only our id out) - update ...\ExtensionInstallAllowlist: one row with our id -- only with -WriteAllowlist (this machine's registry) -- for environments that block every extension ('*' blocklist) Undo: -Uninstall - create ...\3rdparty\extensions\\policy\tenantCatalog (tenant name, tenant id, app client id, activation defaults; plus -AdditionalTenantsJson tenants) (this machine's registry) -- the extension's 'Use centrally deployed' setup reads it Undo: -Uninstall (removes the whole 3rdparty\extensions\ key) - update ...\3rdparty\extensions\\policy\autoActivateMaxGroups (DWORD) for BOTH PIM Activator extensions (production and test) -- only with -AutoActivateMaxGroups (-1 removes it) (this machine's registry) -- per-device cap on auto-activation, the same for production and test (as the Intune Remediation writes it) Undo: re-run with -AutoActivateMaxGroups -1 - update ...\3rdparty\extensions\\policy\bulkActivateConfirmThreshold (DWORD) for BOTH PIM Activator extensions -- only with -BulkActivateConfirmThreshold (-1 removes it) (this machine's registry) -- per-device 'click again to confirm' threshold for bulk activation (as the Intune Remediation writes it) Undo: re-run with -BulkActivateConfirmThreshold -1 - remove with -Uninstall: our forcelist / allowlist / install-source rows, our ExtensionSettings entry and the 3rdparty\extensions\ key -- nothing of the organisation's (this machine's registry) -- removes the PIM Activator from this machine (the browser uninstalls it on its next start) Undo: run the install again -WhatIf: Reads this machine's policies and prints every registry value it would set or remove (forcelist slot, install source, ExtensionSettings entry, tenant catalog, per-device limits). Nothing is written. A preview of -Scope Machine runs without elevation. Undo: Deploy-PimActivatorClient.ps1 -Uninstall (same -Scope, -Browser and -Channel): removes only this script's rows, entries and keys; the browser removes the extension on its next start. Existing policies: the org's own extension policies are never overwritten: our rows go into the slot already holding our id, else the lowest free slot, and our ExtensionSettings entry is MERGED into the org's dictionary. Same layout and the same code (_PimActivatorHybridPolicy.ps1) as Deploy-PimActivatorHybrid.ps1 -Target LocalGpo, so the two writers cannot disagree. That file must sit next to this one. Tested on Edge for Business 120+ and Chrome 120+. Both browsers pick up policy changes on next launch; no reboot required. A transcript of the run is written to \pim-manager-logs\ (its path is printed). .LINK https://invardia.com/docs/pim/scripts/Deploy-PimActivatorClient/ #> [CmdletBinding(DefaultParameterSetName = 'Install', SupportsShouldProcess)] param( # Extension id is constant across every install of this distribution # (derived from manifest.json "key" field). Default so the vanilla call # works without docs lookup. Override only if you fork the extension. [ValidatePattern('^[a-p]{32}$')] [string]$ExtensionId = 'eheocihmlppcophaeakmdenhgcookkab', # Update manifest is hosted on the published gh-pages of the upstream # repo. Default so single-tenant operators don't need to type the URL. [Parameter(ParameterSetName = 'Install')] [string]$UpdateUrl = 'https://knudsenmorten.github.io/PIM4EntraPS/updates.xml', # Update channel. 'Test' targets the test extension id + updates-test.xml so this # writes the SEPARATE test forcelist + managed catalog (same tenant catalog as # released; only the id/URL differ). 'Released' (default) is unchanged. Mirrors # Update-PimActivator-Extension.ps1. [Parameter()] [ValidateSet('Released','Test')] [string]$Channel = 'Released', [Parameter()] [ValidateSet('Machine', 'User')] [string]$Scope = 'Machine', [Parameter()] [ValidateSet('Edge', 'Chrome', 'Both')] [string]$Browser = 'Both', # Optional path to the tenant catalog JSON. v2.4.111 removed the previous # sibling-file default ('discovered-tenant-catalog.json' next to this # script) after a customer-tenant incident where that file -- left over # in a working dir from a different tenant -- got picked up silently and # written the vendor's own tenant id + client id into the customer's registry. # Now: when omitted (the common case), the script auto-discovers the # tenant + PIM Activator app registration from the LIVE Microsoft Graph # tenant you sign in to (a browser sign-in opens, plain REST, no module, if not already # connected). # That guarantees the catalog matches the tenant the operator is signed # into right now -- never a stale file from elsewhere. Pass an explicit # path to override, or -SkipTenantCatalog to skip the catalog write # entirely (still writes the forcelist / sources / settings policies). [Parameter(ParameterSetName = 'Install')] [string]$CatalogJsonPath, [Parameter(ParameterSetName = 'Install')] [switch]$SkipTenantCatalog, # Opt-in: also write ExtensionInstallAllowlist for our id. Only useful # in environments where the admin has set ExtensionInstallBlocklist='*'. # Default OFF since this caused "can't install any other extension" on # some Chromium versions where the presence of an Allowlist key with # a single entry was interpreted as "deny everything else" (2026-06-10). [Parameter(ParameterSetName = 'Install')] [switch]$WriteAllowlist, # Override the per-tenant activation defaults the popup pre-fills. # -DefaultJustification sets the justification text; -DefaultDurationHours # sets the activation length (whole hours). When supplied they OVERWRITE # whatever the resolved catalog carried -- the value from -CatalogJsonPath # or the auto-discover fallback ('Change in infrastructure' / 8h) -- on # EVERY tenant entry written to the registry. Omit them to keep the # catalog's own values. Additive + opt-in: absent => nothing changes. [Parameter(ParameterSetName = 'Install')] [string]$DefaultJustification, [Parameter(ParameterSetName = 'Install')] [ValidateRange(1, 24)] [int]$DefaultDurationHours, # Per-DEVICE cap on auto-activation (2026-09-23): writes ...\3rdparty\extensions\\policy\ # autoActivateMaxGroups (REG_DWORD). 0 = auto-activation off on this machine; N = at most N groups; # -1 = remove the value (no limit). Not passed = the machine's current value is left alone. # Written for BOTH PIM Activator extensions (production and test), like the Intune Remediation (owner 2026-10-22: # "the script must support the new features like max 3 activations"). [Parameter(ParameterSetName = 'Install')] [ValidateRange(-1, 100)] [int]$AutoActivateMaxGroups, # ACT-ENDTIME (owner 2026-10-31): per-DEVICE end-of-day time, ...\policy\activationEndTime (REG_SZ 'HH:MM'). # 'none' = remove the value. Not passed = left alone. Both extensions, as above. [Parameter(ParameterSetName = 'Install')] [ValidatePattern('^(([01][0-9]|2[0-3]):[0-5][0-9]|none)$')] [string]$ActivationEndTime, # Per-DEVICE "click again to confirm" threshold for bulk activation: ...\policy\bulkActivateConfirmThreshold # (REG_DWORD). 1-100 = the threshold; -1 = remove the value (the extension's default). Not passed = left alone. # A tenant's own bulkActivateConfirmThreshold in the catalog wins over it. Both extensions, as above. [Parameter(ParameterSetName = 'Install')] [ValidateRange(-1, 100)] [int]$BulkActivateConfirmThreshold, # Optional name filters for THIS tenant's catalog entry (with -TenantId / -ClientId), the same as the Remediation's: # the popup lists only groups / Entra roles / Azure roles whose names start with them. [Parameter(ParameterSetName = 'Install')] [string]$Prefix = '', [Parameter(ParameterSetName = 'Install')] [string]$EntraPrefix = '', [Parameter(ParameterSetName = 'Install')] [string]$AzurePrefix = '', [Parameter(ParameterSetName = 'Install')] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string]$ClientId, [Parameter(ParameterSetName = 'Install')] [string]$AppDisplayName = 'PIM Activator', # 97.2 (owner 2026-10-08, "one download + one command, nothing else"): with -TenantId AND -ClientId the tenant # catalog is written from these values directly -- no sign-in, no -CatalogJsonPath file. -TenantName is the name # admins see (default 'PIM'). The PIM Manager's Operations > PIM Activator page fills all three. [Parameter(ParameterSetName = 'Install')] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string]$TenantId, [Parameter(ParameterSetName = 'Install')] [string]$TenantName = 'PIM', # Optional multi-tenant catalog: more tenants (JSON array of { name, tenantId, clientId, ... }) added after this one. [Parameter(ParameterSetName = 'Install')] [string]$AdditionalTenantsJson = '', [Parameter(Mandatory, ParameterSetName = 'Uninstall')] [switch]$Uninstall ) $ErrorActionPreference = 'Stop' # 12.7 run frame: "