#requires -Version 5.1 <# .SYNOPSIS Validates and applies Emprise's managed Cofia policy on one Windows device. .DESCRIPTION Run with native 64-bit PowerShell as administrator or SYSTEM while Cofia is closed in every session. The adjacent JSON is secret-bearing data, not an importable registry file. Keep it in the approved MDM delivery channel and do not run in a transcribed/debug-traced shell. No credentials are arguments. The BIOS serial replaces ManagedDeviceId in memory; use -DeviceId for a stable IT-assigned identity when the BIOS serial is generic or unsuitable. Writes use an invalid deployment-classification marker until the full policy is verified. An interrupted update leaves capture blocked; rerun this script. This script does not install, start, stop, or uninstall Cofia. Use -AxPseudonymizationPath for the portal's capture-policy repair download. Repair requires an existing complete Emprise policy and preserves its OAuth credentials and device identity. It cannot rotate an existing capture key. .EXAMPLE & .\Set-EmprisePolicy.ps1 -ValidateOnly .EXAMPLE & .\Set-EmprisePolicy.ps1 -PolicyPath 'C:\ProgramData\Emprise\emprise-windows-policy.json' .EXAMPLE & .\Set-EmprisePolicy.ps1 -AxPseudonymizationPath 'C:\ProgramData\Emprise\emprise-capture-policy.json' #> [CmdletBinding(DefaultParameterSetName = 'Full')] param( [Parameter(ParameterSetName = 'Full')] [string] $PolicyPath, [Parameter(ParameterSetName = 'Full')] [string] $DeviceId, [Parameter(Mandatory = $true, ParameterSetName = 'CaptureRepair')] [string] $AxPseudonymizationPath, [switch] $ValidateOnly ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $failureMessage = 'Emprise policy validation failed; no policy values were printed.' $registryRoot = $null $policyKey = $null $writeStarted = $false $complete = $false function Assert-ExactProperties { param([object] $Value, [string[]] $Names) if ($Value -isnot [System.Management.Automation.PSCustomObject]) { throw 'Invalid object.' } $actual = @($Value.PSObject.Properties | ForEach-Object { $_.Name }) if ($actual.Count -ne $Names.Count) { throw 'Invalid property count.' } foreach ($name in $Names) { if ($actual -cnotcontains $name) { throw 'Missing property.' } } } function Assert-CofiaStopped { # Enumerate all sessions. Do not use SilentlyContinue: an inspection # failure must not be confused with an absent application. $running = @(Get-Process -ErrorAction Stop | Where-Object { $_.ProcessName -ieq 'Cofia' -or $_.ProcessName -ieq 'Lighthouse' }) if ($running.Count -gt 0) { throw 'Client is running.' } } function Assert-AxPseudonymization { param([object] $Value, [string] $ManagedClientSecret) $keyBytes = $null try { if ($Value -isnot [string] -or $Value.Length -gt 4096) { throw 'Invalid capture policy.' } # The portal emits a flat object of four unescaped string fields. # Validate its lexical form as well as the parsed properties so that # Windows PowerShell 5.1 cannot silently accept duplicate JSON names. $pair = '"(?:deploymentId|tenantId|keyId|keyBase64)"\s*:\s*"[A-Za-z0-9+/.=_-]*"' if ($Value -cnotmatch ('\A\s*\{\s*(?:' + $pair + '\s*,\s*){3}' + $pair + '\s*\}\s*\z')) { throw 'Invalid capture policy.' } $capture = $Value | ConvertFrom-Json -ErrorAction Stop Assert-ExactProperties $capture @('deploymentId', 'tenantId', 'keyId', 'keyBase64') if ($capture.deploymentId -cne 'phi-emprise-prod' -or $capture.tenantId -cne '8b473e2c-0564-4265-b3fa-037e8c5f8fac' -or $capture.keyId -cnotmatch '\A[A-Za-z0-9._-]{1,128}\z' -or $capture.keyBase64.Length -ne 44 -or $capture.keyBase64 -ceq $ManagedClientSecret) { throw 'Invalid capture binding or key.' } $keyBytes = [Convert]::FromBase64String($capture.keyBase64) if ($keyBytes.Length -ne 32 -or [Convert]::ToBase64String($keyBytes) -cne $capture.keyBase64 -or @($keyBytes | Where-Object { $_ -ne 0 }).Count -eq 0) { throw 'Invalid capture key.' } } catch { throw 'Invalid capture policy.' } finally { if ($null -ne $keyBytes) { [Array]::Clear($keyBytes, 0, $keyBytes.Length) } $capture = $null } } function Assert-SameCaptureKey { param([object] $Existing, [string] $Requested, [string] $ManagedClientSecret) if ($null -eq $Existing) { return } Assert-AxPseudonymization $Existing $ManagedClientSecret $before = $Existing | ConvertFrom-Json -ErrorAction Stop $after = $Requested | ConvertFrom-Json -ErrorAction Stop if ($before.keyId -cne $after.keyId -or $before.keyBase64 -cne $after.keyBase64) { throw 'Capture key rotation requires a separate procedure.' } } try { $failureMessage = 'Use native 64-bit Windows PowerShell as administrator or SYSTEM.' if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitOperatingSystem -or -not [Environment]::Is64BitProcess) { throw 'Unsupported execution context.' } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() try { $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Elevation required.' } } finally { $identity.Dispose() } $failureMessage = 'The adjacent policy path could not be resolved. Supply -PolicyPath with the policy file path.' $captureRepair = $PSCmdlet.ParameterSetName -ceq 'CaptureRepair' if ($captureRepair) { $PolicyPath = $AxPseudonymizationPath } elseif (-not $PSBoundParameters.ContainsKey('PolicyPath')) { # Resolve after parameter binding. Windows PowerShell 5.1 launch paths # can leave PSScriptRoot empty while evaluating a parameter default. # Never fall back to the working directory for secret-bearing input. $scriptFile = $PSCommandPath if ([string]::IsNullOrWhiteSpace($scriptFile)) { $scriptFile = $MyInvocation.MyCommand.Path } if ([string]::IsNullOrWhiteSpace($scriptFile)) { throw 'No script path.' } $PolicyPath = Join-Path -Path (Split-Path -Parent $scriptFile) -ChildPath 'emprise-windows-policy.json' } if ([string]::IsNullOrWhiteSpace($PolicyPath)) { throw 'No policy path.' } $failureMessage = 'The policy file is missing, unreadable, oversized, or invalid JSON.' $source = Get-Item -LiteralPath $PolicyPath -ErrorAction Stop if ($source -isnot [IO.FileInfo] -or $source.Length -gt 65536) { throw 'Invalid file.' } $policy = [IO.File]::ReadAllText($source.FullName) | ConvertFrom-Json -ErrorAction Stop $failureMessage = 'The policy must contain the complete, exact Emprise deployment and managed credential fields.' Assert-ExactProperties $policy @('policyPath', 'values', 'dwordValues') if ($policy.policyPath -isnot [string] -or $policy.policyPath -cne 'HKLM\Software\Policies\Cofia') { throw 'Unexpected policy path.' } $expected = [ordered]@{ DeploymentClassification = 'phi' DeploymentId = 'phi-emprise-prod' ApiEndpoint = 'https://lighthouse.emprise.cofia.ai' CognitoIssuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_1szmFEFRK' CognitoUserPoolId = 'us-east-1_1szmFEFRK' CognitoClientId = '1kc3arpvj35k6j5g9indusl0m2' CognitoHostedUiDomain = 'phi-emprise-prod-auth.auth.us-east-1.amazoncognito.com' TenantMode = 'single-tenant' TenantId = '8b473e2c-0564-4265-b3fa-037e8c5f8fac' ManagedCognitoTokenEndpoint = 'https://phi-emprise-prod-auth.auth.us-east-1.amazoncognito.com/oauth2/token' } $legacyNames = @($expected.Keys) + @('ManagedCognitoClientId', 'ManagedCognitoClientSecret', 'ManagedDeviceId') $names = $legacyNames + @('AxPseudonymization') if ($captureRepair) { Assert-ExactProperties $policy.values @('AxPseudonymization') Assert-ExactProperties $policy.dwordValues @() Assert-AxPseudonymization $policy.values.AxPseudonymization '' $failureMessage = 'Capture repair requires a complete existing Emprise policy with unchanged credentials and device identity.' $registryRoot = [Microsoft.Win32.RegistryKey]::OpenBaseKey( [Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) $policyKey = $registryRoot.OpenSubKey('Software\Policies\Cofia', -not $ValidateOnly) if ($null -eq $policyKey) { throw 'Existing policy is missing.' } $existingNames = @($policyKey.GetValueNames()) $hasCaptureKey = $existingNames -ccontains 'AxPseudonymization' $requiredNames = $legacyNames + @('UpdatesDisabled') if ($hasCaptureKey) { $requiredNames += 'AxPseudonymization' } if ($existingNames.Count -ne $requiredNames.Count) { throw 'Unexpected existing policy values.' } foreach ($name in $requiredNames) { if ($existingNames -cnotcontains $name) { throw 'Existing policy is incomplete.' } } $existingValues = [ordered]@{} foreach ($name in $legacyNames) { if ($policyKey.GetValueKind($name) -ne [Microsoft.Win32.RegistryValueKind]::String) { throw 'Invalid existing policy type.' } $existingValues[$name] = $policyKey.GetValue($name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } if ($policyKey.GetValueKind('UpdatesDisabled') -ne [Microsoft.Win32.RegistryValueKind]::DWord -or $policyKey.GetValue('UpdatesDisabled') -ne 1) { throw 'Updates must be disabled.' } # A prior interrupted repair may leave this marker. Every other # field must still pass the complete Emprise policy checks below. if ($existingValues.DeploymentClassification -ceq 'configuration-in-progress') { $existingValues.DeploymentClassification = 'phi' } if ($hasCaptureKey) { if ($policyKey.GetValueKind('AxPseudonymization') -ne [Microsoft.Win32.RegistryValueKind]::String) { throw 'Invalid existing capture policy type.' } Assert-SameCaptureKey ($policyKey.GetValue('AxPseudonymization')) $policy.values.AxPseudonymization $existingValues.ManagedCognitoClientSecret } $existingValues.AxPseudonymization = $policy.values.AxPseudonymization $policy.values = [pscustomobject]$existingValues $policy.dwordValues = [pscustomobject]@{ UpdatesDisabled = 1 } } Assert-ExactProperties $policy.values $names Assert-ExactProperties $policy.dwordValues @('UpdatesDisabled') if (($policy.dwordValues.UpdatesDisabled -isnot [int] -and $policy.dwordValues.UpdatesDisabled -isnot [long]) -or $policy.dwordValues.UpdatesDisabled -ne 1) { throw 'Updates must be disabled.' } foreach ($name in $expected.Keys) { if ($policy.values.$name -isnot [string] -or $policy.values.$name -cne $expected[$name]) { throw 'Deployment mismatch.' } } $failureMessage = 'Device identity could not be resolved. Supply a stable IT-assigned -DeviceId if the BIOS serial is unsuitable.' if ($captureRepair) { $resolvedDeviceId = $policy.values.ManagedDeviceId } elseif ($PSBoundParameters.ContainsKey('DeviceId')) { $resolvedDeviceId = $DeviceId } else { $bios = @(Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop) if ($bios.Count -ne 1 -or $bios[0].SerialNumber -isnot [string]) { throw 'No single serial.' } $resolvedDeviceId = $bios[0].SerialNumber.Trim() } if ([string]::IsNullOrWhiteSpace($resolvedDeviceId) -or $resolvedDeviceId -cnotmatch '\A[A-Za-z0-9][A-Za-z0-9._:/+ -]{0,127}\z' -or $resolvedDeviceId -match '\A(?:0+|unknown|none|default string|system serial number|to be filled by o\.?e\.?m\.?|not (?:specified|applicable|available))\z') { throw 'Invalid device identity.' } $policy.values.ManagedDeviceId = $resolvedDeviceId $failureMessage = 'Managed credentials or device identity are incomplete, malformed, or still contain placeholders.' foreach ($name in $names) { $value = $policy.values.$name if ($value -isnot [string] -or [string]::IsNullOrWhiteSpace($value) -or $value.Length -gt 2048 -or $value -match '[\x00-\x1f\x7f]' -or $value -match '(?i:REPLACE_ME|RESOLVED_ON_DEVICE|PLACEHOLDER|\{\{|\}\}|\$SERIALNUMBER)') { throw 'Invalid policy value.' } } if ($policy.values.ManagedCognitoClientId -cnotmatch '\A[A-Za-z0-9]{1,128}\z' -or $policy.values.ManagedCognitoClientId -ceq $expected.CognitoClientId -or $policy.values.ManagedCognitoClientSecret -match '\s' -or $policy.values.ManagedCognitoClientSecret.Length -gt 256) { throw 'Invalid managed credential.' } $failureMessage = 'The capture policy is missing, malformed, bound to another deployment, or reuses the managed credential.' Assert-AxPseudonymization $policy.values.AxPseudonymization $policy.values.ManagedCognitoClientSecret $failureMessage = 'Close Cofia in every user session, then rerun; the script does not stop applications.' Assert-CofiaStopped if ($ValidateOnly) { Write-Output 'Emprise policy validation passed. No registry changes were made.' } else { $failureMessage = 'The existing Cofia policy belongs to another deployment or could not be inspected; no update was started.' if ($null -eq $registryRoot) { $registryRoot = [Microsoft.Win32.RegistryKey]::OpenBaseKey( [Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) $policyKey = $registryRoot.OpenSubKey('Software\Policies\Cofia', $true) } if ($null -ne $policyKey) { $existingDeployment = $policyKey.GetValue('DeploymentId', $null) if ($null -ne $existingDeployment -and $existingDeployment -cne 'phi-emprise-prod') { throw 'Other deployment exists.' } $failureMessage = 'The existing capture key is invalid or different. Keep its stable key when updating credentials; key rotation requires a separate procedure.' if (-not $captureRepair -and (@($policyKey.GetValueNames()) -contains 'AxPseudonymization')) { if ($policyKey.GetValueKind('AxPseudonymization') -ne [Microsoft.Win32.RegistryValueKind]::String) { throw 'Invalid existing capture policy type.' } Assert-SameCaptureKey ($policyKey.GetValue('AxPseudonymization')) $policy.values.AxPseudonymization $policy.values.ManagedCognitoClientSecret } } $failureMessage = 'Close Cofia in every user session, then rerun; the script does not stop applications.' Assert-CofiaStopped $failureMessage = 'Policy application failed. Keep Cofia closed and rerun this script to repair the incomplete policy.' if ($null -eq $policyKey) { $policyKey = $registryRoot.CreateSubKey('Software\Policies\Cofia') } $writeStarted = $true $policyKey.SetValue('DeploymentClassification', 'configuration-in-progress', [Microsoft.Win32.RegistryValueKind]::String) $writeNames = $names if ($captureRepair) { $writeNames = @('AxPseudonymization') } foreach ($name in $writeNames) { if ($name -cne 'DeploymentClassification') { $policyKey.SetValue($name, $policy.values.$name, [Microsoft.Win32.RegistryValueKind]::String) } } if (-not $captureRepair) { $policyKey.SetValue('UpdatesDisabled', 1, [Microsoft.Win32.RegistryValueKind]::DWord) } foreach ($name in $names) { if ($name -cne 'DeploymentClassification' -and ($policyKey.GetValueKind($name) -ne [Microsoft.Win32.RegistryValueKind]::String -or $policyKey.GetValue($name) -cne $policy.values.$name)) { throw 'Write verification failed.' } } if ($policyKey.GetValueKind('UpdatesDisabled') -ne [Microsoft.Win32.RegistryValueKind]::DWord -or $policyKey.GetValue('UpdatesDisabled') -ne 1) { throw 'Write verification failed.' } $policyKey.SetValue('DeploymentClassification', 'phi', [Microsoft.Win32.RegistryValueKind]::String) $policyKey.Flush() if ($policyKey.GetValueKind('DeploymentClassification') -ne [Microsoft.Win32.RegistryValueKind]::String -or $policyKey.GetValue('DeploymentClassification') -cne 'phi') { throw 'Write verification failed.' } $complete = $true Write-Output 'Emprise policy applied and read back successfully. Cofia can be launched in the user session.' } } catch { if ($writeStarted -and -not $complete -and $null -ne $policyKey) { try { $policyKey.SetValue('DeploymentClassification', 'configuration-in-progress', [Microsoft.Win32.RegistryValueKind]::String) $policyKey.Flush() } catch { # Keep the fixed failure message; never emit registry data or exception text. } } [Console]::Error.WriteLine($failureMessage) exit 1 } finally { if ($null -ne $policyKey) { $policyKey.Dispose() } if ($null -ne $registryRoot) { $registryRoot.Dispose() } $policy = $null }