Public/Check-OwnerLensPrerequisites.ps1
|
<#
.SYNOPSIS Checks whether the local machine is ready to run and collect data with OwnerLens. .DESCRIPTION Validates Windows/PowerShell prerequisites, module dependencies, local filesystem paths, OwnerLens packaged/source runtime layout, Node.js availability, Microsoft Graph authentication, Azure authentication, subscription access, and optional local runtime startup. The command is read-only except for creating/removing tiny probe files in DataPath, LOCALAPPDATA, and TEMP. If -TestRuntimeStartup is used, it starts OwnerLens on 127.0.0.1 with a temporary runtime token and stops it after probing /api/data. .EXAMPLE Check-OwnerLensPrerequisites -SkipGraph -SkipAzure Checks local system and runtime prerequisites without checking Microsoft Graph or Azure authentication. .EXAMPLE Check-OwnerLensPrerequisites -OutputJson -FailOnError Writes the report as JSON and throws when one or more prerequisite checks fail. #> function Check-OwnerLensPrerequisites { [CmdletBinding()] param( [string]$RuntimePath = "", [string]$PackageRoot = "", [ValidateNotNullOrEmpty()] [string]$DataPath = (Join-Path (Get-Location) "data"), [string]$TenantId = "", [string]$SubscriptionIds = "", [ValidateRange(0, 65535)] [int]$Port = 0, [ValidateRange(0, 1024)] [int]$MinimumFreeDiskGB = 2, [switch]$SkipGraph, [switch]$SkipAzure, [switch]$SkipRuntime, [switch]$SkipOnlineChecks, [switch]$TestRuntimeStartup, [switch]$OutputJson, [switch]$FailOnError ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $checks = [System.Collections.Generic.List[object]]::new() function Add-OwnerLensCheck { param( [ValidateSet("Pass", "Warn", "Fail", "Info")] [string]$Status, [Parameter(Mandatory = $true)] [string]$Area, [Parameter(Mandatory = $true)] [string]$Name, [string]$Details = "", [string]$Fix = "", [object]$Data = $null ) $checks.Add([pscustomobject]@{ Status = $Status Area = $Area Name = $Name Details = $Details Fix = $Fix Data = $Data }) | Out-Null } function Test-OwnerLensCommand { param([Parameter(Mandatory = $true)][string]$Name) try { return Get-Command $Name -ErrorAction Stop } catch { return $null } } function Get-OwnerLensBestModule { param([Parameter(Mandatory = $true)][string]$Name) try { return Get-Module -ListAvailable -Name $Name | Sort-Object Version -Descending | Select-Object -First 1 } catch { return $null } } function Test-OwnerLensModule { param( [Parameter(Mandatory = $true)][string]$Name, [Parameter(Mandatory = $true)][string]$Area, [string]$InstallHint = "", [switch]$Required ) $module = Get-OwnerLensBestModule -Name $Name if (-not $module) { Add-OwnerLensCheck ` -Status $(if ($Required) { "Fail" } else { "Warn" }) ` -Area $Area ` -Name "PowerShell module: $Name" ` -Details "Module not found." ` -Fix $(if ($InstallHint) { $InstallHint } else { "Install-Module $Name -Scope CurrentUser" }) return $null } try { Import-Module $Name -ErrorAction Stop Add-OwnerLensCheck -Status "Pass" -Area $Area -Name "PowerShell module: $Name" -Details "Loaded $($module.Version) from $($module.ModuleBase)." } catch { Add-OwnerLensCheck ` -Status $(if ($Required) { "Fail" } else { "Warn" }) ` -Area $Area ` -Name "PowerShell module import: $Name" ` -Details $_.Exception.Message ` -Fix $(if ($InstallHint) { $InstallHint } else { "Reinstall module: Install-Module $Name -Scope CurrentUser -Force" }) } return $module } function Resolve-OwnerLensPath { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return "" } try { return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } catch { return [System.IO.Path]::GetFullPath($Path) } } function Test-OwnerLensWritableDirectory { param( [Parameter(Mandatory = $true)][string]$Area, [Parameter(Mandatory = $true)][string]$Name, [Parameter(Mandatory = $true)][string]$Path, [switch]$Create ) try { $resolvedPath = Resolve-OwnerLensPath -Path $Path if (-not (Test-Path -LiteralPath $resolvedPath)) { if ($Create) { New-Item -ItemType Directory -Path $resolvedPath -Force | Out-Null } else { Add-OwnerLensCheck -Status "Fail" -Area $Area -Name $Name -Details "Directory does not exist: $resolvedPath" -Fix "Create the directory or pass a different path." return } } $probe = Join-Path $resolvedPath (".ownerlens-prereq-{0}.tmp" -f ([guid]::NewGuid().ToString("N"))) "ownerlens" | Set-Content -LiteralPath $probe -Encoding UTF8 Remove-Item -LiteralPath $probe -Force Add-OwnerLensCheck -Status "Pass" -Area $Area -Name $Name -Details "Writable: $resolvedPath" } catch { Add-OwnerLensCheck -Status "Fail" -Area $Area -Name $Name -Details $_.Exception.Message -Fix "Grant write access or use another path." } } function Test-OwnerLensDiskFree { param( [Parameter(Mandatory = $true)][string]$Path, [int]$MinimumGB ) try { $resolvedPath = Resolve-OwnerLensPath -Path $Path if (-not (Test-Path -LiteralPath $resolvedPath)) { New-Item -ItemType Directory -Path $resolvedPath -Force | Out-Null } $root = [System.IO.Path]::GetPathRoot($resolvedPath) if (-not $root) { Add-OwnerLensCheck -Status "Warn" -Area "Storage" -Name "Free disk space" -Details "Could not determine drive root for $resolvedPath." return } $driveName = $root.TrimEnd("\") $drive = Get-PSDrive -Name $driveName.TrimEnd(":") -ErrorAction Stop $freeGB = [math]::Round(($drive.Free / 1GB), 2) $status = if ($freeGB -ge $MinimumGB) { "Pass" } else { "Warn" } Add-OwnerLensCheck -Status $status -Area "Storage" -Name "Free disk space" -Details "$freeGB GB free on $root. Minimum expected: $MinimumGB GB." -Fix $(if ($status -eq "Warn") { "Use a drive with more free space for large tenant snapshots." } else { "" }) } catch { Add-OwnerLensCheck -Status "Warn" -Area "Storage" -Name "Free disk space" -Details $_.Exception.Message } } function Find-OwnerLensPackageRoot { param([string]$ExplicitPackageRoot) $candidates = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrWhiteSpace($ExplicitPackageRoot)) { $candidates.Add((Resolve-OwnerLensPath -Path $ExplicitPackageRoot)) | Out-Null } $candidates.Add((Get-Location).ProviderPath) | Out-Null if ($PSScriptRoot) { $cursor = Resolve-OwnerLensPath -Path $PSScriptRoot for ($i = 0; $i -lt 6 -and -not [string]::IsNullOrWhiteSpace($cursor); $i++) { $candidates.Add($cursor) | Out-Null $parent = Split-Path -Parent $cursor if ($parent -eq $cursor) { break } $cursor = $parent } } foreach ($candidate in ($candidates | Select-Object -Unique)) { if ( (Test-Path -LiteralPath (Join-Path $candidate "package.json")) -and (Test-Path -LiteralPath (Join-Path $candidate "bin\ownerlens.js")) ) { return $candidate } } return "" } function Find-OwnerLensModuleRoot { $candidates = [System.Collections.Generic.List[string]]::new() if ($PSScriptRoot) { $cursor = Resolve-OwnerLensPath -Path $PSScriptRoot for ($i = 0; $i -lt 6 -and -not [string]::IsNullOrWhiteSpace($cursor); $i++) { $candidates.Add($cursor) | Out-Null $candidates.Add((Join-Path $cursor "powershell\OwnerLens")) | Out-Null $parent = Split-Path -Parent $cursor if ($parent -eq $cursor) { break } $cursor = $parent } } $module = Get-OwnerLensBestModule -Name "OwnerLens" if ($module) { $candidates.Add($module.ModuleBase) | Out-Null } foreach ($candidate in ($candidates | Select-Object -Unique)) { if ( (Test-Path -LiteralPath (Join-Path $candidate "OwnerLens.psd1")) -and (Test-Path -LiteralPath (Join-Path $candidate "OwnerLens.psm1")) ) { return $candidate } } return "" } function Resolve-OwnerLensRuntimeLayout { param( [string]$ExplicitRuntimePath, [string]$PackageRoot, [string]$ModuleRoot ) $candidates = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrWhiteSpace($ExplicitRuntimePath)) { $candidates.Add((Resolve-OwnerLensPath -Path $ExplicitRuntimePath)) | Out-Null } if (-not [string]::IsNullOrWhiteSpace($ModuleRoot)) { $candidates.Add((Join-Path $ModuleRoot "bin\win-x64")) | Out-Null } if ($env:LOCALAPPDATA) { $candidates.Add((Join-Path $env:LOCALAPPDATA "OwnerLens\runtime")) | Out-Null } if (-not [string]::IsNullOrWhiteSpace($PackageRoot)) { $candidates.Add($PackageRoot) | Out-Null } foreach ($candidate in ($candidates | Select-Object -Unique)) { if (-not (Test-Path -LiteralPath $candidate)) { continue } $packagedEntrypoint = Join-Path $candidate "app\bin\ownerlens.js" $sourceEntrypoint = Join-Path $candidate "bin\ownerlens.js" if (Test-Path -LiteralPath $packagedEntrypoint) { return [pscustomobject]@{ Kind = "PackagedRuntime" RuntimeRoot = $candidate AppRoot = Join-Path $candidate "app" Entrypoint = $packagedEntrypoint NodePath = Join-Path $candidate "node.exe" } } if (Test-Path -LiteralPath $sourceEntrypoint) { return [pscustomobject]@{ Kind = "SourcePackage" RuntimeRoot = $candidate AppRoot = $candidate Entrypoint = $sourceEntrypoint NodePath = "" } } } return $null } function Get-OwnerLensNodeVersion { param([Parameter(Mandatory = $true)][string]$NodePath) try { $output = & $NodePath --version 2>$null if ($LASTEXITCODE -ne 0) { return $null } return [string]$output } catch { return $null } } function ConvertTo-OwnerLensNodeVersionNumber { param([string]$Version) if ([string]::IsNullOrWhiteSpace($Version)) { return $null } $clean = $Version.Trim().TrimStart("v") try { return [version]$clean } catch { return $null } } function Test-OwnerLensRuntimeLayout { param([Parameter(Mandatory = $true)]$Layout) Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Runtime layout" -Details "$($Layout.Kind): $($Layout.RuntimeRoot)" $requiredPaths = @( @{ Name = "entrypoint"; Path = $Layout.Entrypoint }, @{ Name = "package.json"; Path = (Join-Path $Layout.AppRoot "package.json") }, @{ Name = "dist/index.html"; Path = (Join-Path $Layout.AppRoot "dist\index.html") }, @{ Name = "dist-server/ownerlens-server.js"; Path = (Join-Path $Layout.AppRoot "dist-server\ownerlens-server.js") }, @{ Name = "node_modules"; Path = (Join-Path $Layout.AppRoot "node_modules") }, @{ Name = "migrations"; Path = (Join-Path $Layout.AppRoot "migrations") }, @{ Name = "contracts"; Path = (Join-Path $Layout.AppRoot "contracts") } ) foreach ($item in $requiredPaths) { if (Test-Path -LiteralPath $item.Path) { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Runtime path: $($item.Name)" -Details $item.Path } else { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime path: $($item.Name)" -Details "Missing: $($item.Path)" -Fix "Run npm run build and package the runtime, or run Install-OwnerLensRuntime -Force from a complete package." } } $packageJsonPath = Join-Path $Layout.AppRoot "package.json" if (Test-Path -LiteralPath $packageJsonPath) { try { $packageJson = Get-Content -LiteralPath $packageJsonPath -Raw | ConvertFrom-Json Add-OwnerLensCheck -Status "Info" -Area "Runtime" -Name "Package" -Details "$($packageJson.name) $($packageJson.version)" } catch { Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Package metadata" -Details "Could not parse package.json: $($_.Exception.Message)" } } $nodePath = "" if ($Layout.NodePath -and (Test-Path -LiteralPath $Layout.NodePath)) { $nodePath = $Layout.NodePath Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Bundled node.exe" -Details $nodePath } else { $nodeCommand = Test-OwnerLensCommand -Name "node.exe" if (-not $nodeCommand) { $nodeCommand = Test-OwnerLensCommand -Name "node" } if ($nodeCommand) { $nodePath = $nodeCommand.Source Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "System Node.js" -Details $nodePath } else { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Node.js" -Details "No bundled node.exe and no node command in PATH." -Fix "Use a packaged Windows runtime with node.exe or install Node.js 20+." } } if ($nodePath) { $nodeVersionRaw = Get-OwnerLensNodeVersion -NodePath $nodePath $nodeVersion = ConvertTo-OwnerLensNodeVersionNumber -Version $nodeVersionRaw if (-not $nodeVersion) { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Node.js version" -Details "Could not read Node.js version from $nodePath." } elseif ($nodeVersion.Major -lt 20) { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Node.js version" -Details "$nodeVersionRaw. Expected Node.js 20+." -Fix "Upgrade Node.js or bundle a current node.exe." } else { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Node.js version" -Details $nodeVersionRaw } } $duckDbApi = Join-Path $Layout.AppRoot "node_modules\@duckdb\node-api" $duckDbBindings = Join-Path $Layout.AppRoot "node_modules\@duckdb\node-bindings" if (Test-Path -LiteralPath $duckDbApi) { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB node-api" -Details $duckDbApi } else { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "DuckDB node-api" -Details "Missing @duckdb/node-api." -Fix "Run npm ci for source layout or rebuild packaged runtime with production dependencies." } if (Test-Path -LiteralPath $duckDbBindings) { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB node-bindings" -Details $duckDbBindings } else { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "DuckDB node-bindings" -Details "Missing @duckdb/node-bindings." -Fix "Run npm ci for source layout or rebuild packaged runtime with production dependencies." } if ($IsWindows) { $winBindingCandidates = @( (Join-Path $Layout.AppRoot "node_modules\@duckdb\node-bindings-win32-x64\duckdb.node") (Join-Path $Layout.AppRoot "node_modules\@duckdb\node-bindings-win32-arm64\duckdb.node") ) $winBinding = $winBindingCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if ($winBinding) { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB Windows native binding" -Details $winBinding } else { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "DuckDB Windows native binding" -Details "Missing @duckdb Windows native binding package." -Fix "Rebuild/package on Windows x64, or ensure optional dependencies are included." } } else { $nativeBinding = Get-ChildItem -LiteralPath (Join-Path $Layout.AppRoot "node_modules\@duckdb") -Recurse -Filter "duckdb.node" -ErrorAction SilentlyContinue | Select-Object -First 1 if ($nativeBinding) { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "DuckDB native binding" -Details $nativeBinding.FullName } else { Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "DuckDB native binding" -Details "No duckdb.node found. This may fail at runtime." } } return $nodePath } function Get-OwnerLensFreePort { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 0) try { $listener.Start() return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() } } function Test-OwnerLensPortAvailable { param([int]$PortToCheck) if ($PortToCheck -le 0) { try { $freePort = Get-OwnerLensFreePort Add-OwnerLensCheck -Status "Pass" -Area "Network" -Name "Loopback bind" -Details "Able to bind 127.0.0.1. Sample free port: $freePort." } catch { Add-OwnerLensCheck -Status "Fail" -Area "Network" -Name "Loopback bind" -Details $_.Exception.Message -Fix "Check local firewall/security software and loopback policy." } return } $listener = $null try { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $PortToCheck) $listener.Start() Add-OwnerLensCheck -Status "Pass" -Area "Network" -Name "Port $PortToCheck" -Details "Available on 127.0.0.1." } catch { Add-OwnerLensCheck -Status "Fail" -Area "Network" -Name "Port $PortToCheck" -Details $_.Exception.Message -Fix "Stop the process using the port or run Start-OwnerLens -Port 0." } finally { if ($listener) { $listener.Stop() } } } function Test-OwnerLensRuntimeStartup { param( [Parameter(Mandatory = $true)]$Layout, [Parameter(Mandatory = $true)][string]$NodePath, [Parameter(Mandatory = $true)][string]$DataPath, [int]$RequestedPort ) if (-not $NodePath) { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime startup" -Details "Skipped because Node.js is not available." return } $portToUse = if ($RequestedPort -gt 0) { $RequestedPort } else { Get-OwnerLensFreePort } $token = [guid]::NewGuid().ToString("N") $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) ("ownerlens-prereq-{0}.out.log" -f $token) $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) ("ownerlens-prereq-{0}.err.log" -f $token) $process = $null try { $startInfo = [System.Diagnostics.ProcessStartInfo]::new() $startInfo.FileName = $NodePath $startInfo.WorkingDirectory = $Layout.AppRoot $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $startInfo.ArgumentList.Add($Layout.Entrypoint) $startInfo.ArgumentList.Add("start") $startInfo.ArgumentList.Add("--host") $startInfo.ArgumentList.Add("127.0.0.1") $startInfo.ArgumentList.Add("--port") $startInfo.ArgumentList.Add([string]$portToUse) $startInfo.Environment["OWNERLENS_DATA_DIR"] = (Resolve-OwnerLensPath -Path $DataPath) $startInfo.Environment["OWNERLENS_RUNTIME_TOKEN"] = $token $process = [System.Diagnostics.Process]::Start($startInfo) if (-not $process) { throw "Process did not start." } $deadline = (Get-Date).AddSeconds(30) $url = "http://127.0.0.1:$portToUse/api/data" $lastError = "" while ((Get-Date) -lt $deadline) { if ($process.HasExited) { $stdout = $process.StandardOutput.ReadToEnd() $stderr = $process.StandardError.ReadToEnd() throw "Runtime exited with code $($process.ExitCode). stdout=$stdout stderr=$stderr" } try { $response = Invoke-RestMethod -Method GET -Uri $url -Headers @{ "x-ownerlens-runtime-token" = $token } -TimeoutSec 5 -ErrorAction Stop Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "Runtime startup" -Details "Started and answered $url. Files=$(@($response.files).Count)." return } catch { $lastError = $_.Exception.Message Start-Sleep -Milliseconds 500 } } throw "Runtime did not become ready within 30 seconds. Last error: $lastError" } catch { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime startup" -Details $_.Exception.Message -Fix "Check runtime build, DuckDB native bindings, logs, and OWNERLENS_DATA_DIR permissions." } finally { if ($process -and -not $process.HasExited) { try { $process.Kill() } catch {} try { $process.WaitForExit(5000) | Out-Null } catch {} } Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue } } function Test-OwnerLensSystem { Add-OwnerLensCheck -Status "Info" -Area "System" -Name "Machine" -Details "$env:COMPUTERNAME / $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)" Add-OwnerLensCheck -Status "Info" -Area "System" -Name "Architecture" -Details ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) if ($IsWindows) { Add-OwnerLensCheck -Status "Pass" -Area "System" -Name "Operating system" -Details "Windows detected." } else { Add-OwnerLensCheck -Status "Fail" -Area "System" -Name "Operating system" -Details "OwnerLens PowerShell module is currently Windows-only." -Fix "Run on Windows with PowerShell 7." } if ($PSVersionTable.PSEdition -eq "Core" -and $PSVersionTable.PSVersion.Major -ge 7) { Add-OwnerLensCheck -Status "Pass" -Area "System" -Name "PowerShell" -Details "$($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)" } else { Add-OwnerLensCheck -Status "Fail" -Area "System" -Name "PowerShell" -Details "$($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion). Expected PowerShell 7+." -Fix "Install PowerShell 7 and run with pwsh." } try { $policies = Get-ExecutionPolicy -List | ForEach-Object { "$($_.Scope)=$($_.ExecutionPolicy)" } $effective = Get-ExecutionPolicy $status = if ($effective -in @("Restricted", "AllSigned")) { "Warn" } else { "Pass" } Add-OwnerLensCheck -Status $status -Area "System" -Name "Execution policy" -Details ($policies -join "; ") -Fix $(if ($status -eq "Warn") { "Run packaged commands via pwsh -ExecutionPolicy Bypass, or adjust CurrentUser policy if allowed." } else { "" }) } catch { Add-OwnerLensCheck -Status "Warn" -Area "System" -Name "Execution policy" -Details $_.Exception.Message } if ($env:LOCALAPPDATA) { Add-OwnerLensCheck -Status "Pass" -Area "System" -Name "LOCALAPPDATA" -Details $env:LOCALAPPDATA } else { Add-OwnerLensCheck -Status "Fail" -Area "System" -Name "LOCALAPPDATA" -Details "Environment variable is empty." -Fix "Run from a normal interactive Windows user profile." } if ($IsWindows) { try { $longPaths = Get-ItemPropertyValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -ErrorAction Stop $status = if ([int]$longPaths -eq 1) { "Pass" } else { "Warn" } Add-OwnerLensCheck -Status $status -Area "System" -Name "Long paths" -Details "LongPathsEnabled=$longPaths" -Fix $(if ($status -eq "Warn") { "Enable Windows long paths if npm/source layouts hit path length issues." } else { "" }) } catch { Add-OwnerLensCheck -Status "Warn" -Area "System" -Name "Long paths" -Details "Could not read registry value: $($_.Exception.Message)" } } Test-OwnerLensWritableDirectory -Area "Storage" -Name "DataPath writable" -Path $DataPath -Create if ($env:LOCALAPPDATA) { Test-OwnerLensWritableDirectory -Area "Storage" -Name "OwnerLens app data writable" -Path (Join-Path $env:LOCALAPPDATA "OwnerLens") -Create } Test-OwnerLensWritableDirectory -Area "Storage" -Name "TEMP writable" -Path ([System.IO.Path]::GetTempPath()) Test-OwnerLensDiskFree -Path $DataPath -MinimumGB $MinimumFreeDiskGB Test-OwnerLensPortAvailable -PortToCheck $Port foreach ($commandName in @("pwsh", "git", "npm")) { $command = Test-OwnerLensCommand -Name $commandName if ($command) { Add-OwnerLensCheck -Status "Info" -Area "System" -Name "Command: $commandName" -Details $command.Source } else { $status = if ($commandName -eq "pwsh") { "Fail" } else { "Warn" } Add-OwnerLensCheck -Status $status -Area "System" -Name "Command: $commandName" -Details "Not found in PATH." } } } function Test-OwnerLensGraph { if ($SkipGraph) { Add-OwnerLensCheck -Status "Info" -Area "Graph" -Name "Graph checks" -Details "Skipped by -SkipGraph." return } Test-OwnerLensModule -Name "Microsoft.Graph.Authentication" -Area "Graph" -Required -InstallHint "Install-Module Microsoft.Graph -Scope CurrentUser" | Out-Null Test-OwnerLensModule -Name "Microsoft.Graph.Applications" -Area "Graph" -Required -InstallHint "Install-Module Microsoft.Graph -Scope CurrentUser" | Out-Null Test-OwnerLensModule -Name "Microsoft.Graph.Groups" -Area "Graph" -InstallHint "Install-Module Microsoft.Graph -Scope CurrentUser" | Out-Null if (-not (Test-OwnerLensCommand -Name "Get-MgContext")) { Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph commands" -Details "Get-MgContext is not available." -Fix "Install/import Microsoft.Graph.Authentication." return } $context = $null try { $context = Get-MgContext } catch {} if (-not $context) { Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph connection" -Details "Not connected." -Fix "Connect-MgGraph -TenantId '<tenant-id>' -Scopes 'Application.Read.All','Group.Read.All','Directory.Read.All'" return } Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Graph connection" -Details "Tenant=$($context.TenantId); Account=$($context.Account); AuthType=$($context.AuthType)" if (-not [string]::IsNullOrWhiteSpace($TenantId) -and $context.TenantId -ne $TenantId) { Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Tenant match" -Details "Connected tenant is $($context.TenantId), expected $TenantId." -Fix "Reconnect: Disconnect-MgGraph; Connect-MgGraph -TenantId '$TenantId' -Scopes ..." } elseif (-not [string]::IsNullOrWhiteSpace($TenantId)) { Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Tenant match" -Details $TenantId } $requiredScopes = @("Application.Read.All", "Group.Read.All", "Directory.Read.All") $grantedScopes = @($context.Scopes) foreach ($scope in $requiredScopes) { if ($grantedScopes -contains $scope) { Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Graph scope: $scope" -Details "Granted." } else { Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph scope: $scope" -Details "Missing. Current scopes: $($grantedScopes -join ', ')" -Fix "Reconnect with scopes: $($requiredScopes -join ', ')" } } if ($SkipOnlineChecks) { Add-OwnerLensCheck -Status "Info" -Area "Graph" -Name "Graph API probes" -Details "Skipped by -SkipOnlineChecks." return } $graphProbes = @( @{ Name = "organization"; Uri = "/v1.0/organization?`$select=id,displayName" }, @{ Name = "service principals"; Uri = "/v1.0/servicePrincipals?`$top=1&`$select=id,displayName,appId" }, @{ Name = "applications"; Uri = "/v1.0/applications?`$top=1&`$select=id,displayName,appId" }, @{ Name = "oauth2PermissionGrants"; Uri = "/v1.0/oauth2PermissionGrants?`$top=1&`$select=id,clientId,resourceId,scope" }, @{ Name = "groups"; Uri = "/v1.0/groups?`$top=1&`$select=id,displayName" } ) foreach ($probe in $graphProbes) { try { Invoke-MgGraphRequest -Method GET -Uri $probe.Uri -OutputType PSObject -ErrorAction Stop | Out-Null Add-OwnerLensCheck -Status "Pass" -Area "Graph" -Name "Graph probe: $($probe.Name)" -Details $probe.Uri } catch { Add-OwnerLensCheck -Status "Fail" -Area "Graph" -Name "Graph probe: $($probe.Name)" -Details $_.Exception.Message -Fix "Check Graph permissions/consent for OwnerLens collection." } } } function Test-OwnerLensAzure { if ($SkipAzure) { Add-OwnerLensCheck -Status "Info" -Area "Azure" -Name "Azure checks" -Details "Skipped by -SkipAzure." return } Test-OwnerLensModule -Name "Az.Accounts" -Area "Azure" -Required -InstallHint "Install-Module Az -Scope CurrentUser" | Out-Null Test-OwnerLensModule -Name "Az.Resources" -Area "Azure" -Required -InstallHint "Install-Module Az -Scope CurrentUser" | Out-Null Test-OwnerLensModule -Name "Az.ManagedServiceIdentity" -Area "Azure" -Required -InstallHint "Install-Module Az.ManagedServiceIdentity -Scope CurrentUser" | Out-Null $requiredCommands = @( "Get-AzContext", "Connect-AzAccount", "Get-AzSubscription", "Set-AzContext", "Invoke-AzRestMethod", "Get-AzResourceGroup", "Get-AzResource", "Get-AzRoleAssignment", "Get-AzUserAssignedIdentity" ) foreach ($commandName in $requiredCommands) { $command = Test-OwnerLensCommand -Name $commandName if ($command) { Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Azure command: $commandName" -Details $command.Source } else { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure command: $commandName" -Details "Missing." -Fix "Install/update Az modules." } } if (-not (Test-OwnerLensCommand -Name "Get-AzContext")) { return } $context = $null try { $context = Get-AzContext } catch {} if (-not $context) { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure connection" -Details "Not connected." -Fix "Connect-AzAccount" return } Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Azure connection" -Details "Account=$($context.Account.Id); Tenant=$($context.Tenant.Id); Subscription=$($context.Subscription.Id) $($context.Subscription.Name)" if (-not [string]::IsNullOrWhiteSpace($TenantId) -and $context.Tenant.Id -ne $TenantId) { Add-OwnerLensCheck -Status "Warn" -Area "Azure" -Name "Azure tenant match" -Details "Azure context tenant is $($context.Tenant.Id), expected $TenantId." -Fix "Set-AzContext to the intended tenant/subscription." } $enabledSubscriptions = @() try { $enabledSubscriptions = @(Get-AzSubscription | Where-Object { $_.State -eq "Enabled" }) Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Enabled subscriptions" -Details "Visible enabled subscriptions: $($enabledSubscriptions.Count)." } catch { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Enabled subscriptions" -Details $_.Exception.Message -Fix "Check Azure login and tenant/subscription permissions." return } $subscriptionFilters = @() if ([string]::IsNullOrWhiteSpace($SubscriptionIds)) { if ($context.Subscription -and $context.Subscription.Id) { $subscriptionFilters = @([string]$context.Subscription.Id) } } else { $subscriptionFilters = @($SubscriptionIds.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } if ($subscriptionFilters.Count -eq 0) { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Subscription selection" -Details "No subscription selected." -Fix "Set-AzContext -SubscriptionId '<id>' or pass -SubscriptionIds." return } $resolvedSubscriptions = @() foreach ($filter in $subscriptionFilters) { $sub = $enabledSubscriptions | Where-Object { $_.Id -eq $filter -or $_.Name -eq $filter } | Select-Object -First 1 if (-not $sub) { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Subscription: $filter" -Details "Not found or not enabled." -Fix "Use an enabled subscription ID/name visible to Get-AzSubscription." continue } $resolvedSubscriptions += $sub Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Subscription: $filter" -Details "$($sub.Name) ($($sub.Id))" } if ($SkipOnlineChecks) { Add-OwnerLensCheck -Status "Info" -Area "Azure" -Name "Azure API probes" -Details "Skipped by -SkipOnlineChecks." return } foreach ($sub in $resolvedSubscriptions) { try { Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop | Out-Null Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Set context: $($sub.Name)" -Details $sub.Id } catch { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Set context: $($sub.Name)" -Details $_.Exception.Message continue } $encodedStart = [Uri]::EscapeDataString((Get-Date).AddMinutes(-15).ToUniversalTime().ToString("o")) $encodedEnd = [Uri]::EscapeDataString((Get-Date).ToUniversalTime().ToString("o")) $activityFilter = [Uri]::EscapeDataString("eventTimestamp ge '$([Uri]::UnescapeDataString($encodedStart))' and eventTimestamp le '$([Uri]::UnescapeDataString($encodedEnd))'") $azureProbes = @( @{ Name = "resource groups"; Path = "/subscriptions/$($sub.Id)/resourcegroups?api-version=2021-04-01" }, @{ Name = "resources"; Path = "/subscriptions/$($sub.Id)/resources?api-version=2021-04-01" }, @{ Name = "role assignments"; Path = "/subscriptions/$($sub.Id)/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$top=1" }, @{ Name = "user-assigned managed identities"; Path = "/subscriptions/$($sub.Id)/providers/Microsoft.ManagedIdentity/userAssignedIdentities?api-version=2023-01-31" }, @{ Name = "activity logs"; Path = "/subscriptions/$($sub.Id)/providers/microsoft.insights/eventtypes/management/values?api-version=2015-04-01&`$filter=$activityFilter" } ) foreach ($probe in $azureProbes) { try { $response = Invoke-AzRestMethod -Method GET -Path $probe.Path -ErrorAction Stop $statusCode = [int]$response.StatusCode if ($statusCode -ge 200 -and $statusCode -lt 300) { Add-OwnerLensCheck -Status "Pass" -Area "Azure" -Name "Azure probe: $($probe.Name) / $($sub.Name)" -Details "HTTP $statusCode" } else { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure probe: $($probe.Name) / $($sub.Name)" -Details "HTTP $statusCode $($response.Content)" -Fix "Check Azure RBAC permissions." } } catch { Add-OwnerLensCheck -Status "Fail" -Area "Azure" -Name "Azure probe: $($probe.Name) / $($sub.Name)" -Details $_.Exception.Message -Fix "Check Azure RBAC permissions and provider availability." } } } } function Test-OwnerLensExistingState { if (-not $env:LOCALAPPDATA) { return } $statePath = Join-Path $env:LOCALAPPDATA "OwnerLens\runtime-state.json" if (-not (Test-Path -LiteralPath $statePath)) { Add-OwnerLensCheck -Status "Info" -Area "Runtime" -Name "Existing runtime state" -Details "No state file found." return } try { $state = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json $process = $null if ($state.ProcessId) { $process = Get-Process -Id ([int]$state.ProcessId) -ErrorAction SilentlyContinue } if ($process) { Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Existing runtime state" -Details "State file points to running PID $($state.ProcessId) at $($state.ServerUrl)." -Fix "Run Stop-OwnerLens before starting a new pilot session if this is stale." } else { Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Stale runtime state" -Details "State file exists but process is not running: $statePath" -Fix "Run Stop-OwnerLens or delete the stale state file." } } catch { Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "Existing runtime state" -Details "Could not parse $statePath`: $($_.Exception.Message)" -Fix "Delete the corrupted state file." } } function Write-OwnerLensReport { $summary = [pscustomobject]@{ Pass = @($checks | Where-Object Status -eq "Pass").Count Warn = @($checks | Where-Object Status -eq "Warn").Count Fail = @($checks | Where-Object Status -eq "Fail").Count Info = @($checks | Where-Object Status -eq "Info").Count } if ($OutputJson) { [pscustomobject]@{ summary = $summary checks = $checks } | ConvertTo-Json -Depth 8 return } Write-Host "" Write-Host "OwnerLens prerequisite check" Write-Host "============================" Write-Host "Pass=$($summary.Pass) Warn=$($summary.Warn) Fail=$($summary.Fail) Info=$($summary.Info)" Write-Host "" $checks | Sort-Object @{ Expression = { switch ($_.Status) { "Fail" { 0 } "Warn" { 1 } "Pass" { 2 } default { 3 } } } }, Area, Name | Select-Object Status, Area, Name, Details, Fix | Format-Table -AutoSize -Wrap $failures = @($checks | Where-Object Status -eq "Fail") if ($failures.Count -gt 0) { Write-Host "" Write-Host "Blocking fixes:" foreach ($failure in $failures) { $fix = if ($failure.Fix) { $failure.Fix } else { "No automatic fix provided." } Write-Host "- [$($failure.Area)] $($failure.Name): $fix" } } } Test-OwnerLensSystem Test-OwnerLensExistingState $moduleRoot = Find-OwnerLensModuleRoot if ($moduleRoot) { Add-OwnerLensCheck -Status "Pass" -Area "Module" -Name "OwnerLens module root" -Details $moduleRoot try { $manifest = Test-ModuleManifest -Path (Join-Path $moduleRoot "OwnerLens.psd1") -ErrorAction Stop Add-OwnerLensCheck -Status "Pass" -Area "Module" -Name "OwnerLens manifest" -Details "Version=$($manifest.Version); PowerShellVersion=$($manifest.PowerShellVersion)" } catch { Add-OwnerLensCheck -Status "Fail" -Area "Module" -Name "OwnerLens manifest" -Details $_.Exception.Message } } else { Add-OwnerLensCheck -Status "Warn" -Area "Module" -Name "OwnerLens module root" -Details "Could not find OwnerLens.psd1/OwnerLens.psm1." -Fix "Run from the repository/package root or install/import the OwnerLens module." } $resolvedPackageRoot = Find-OwnerLensPackageRoot -ExplicitPackageRoot $PackageRoot if ($resolvedPackageRoot) { Add-OwnerLensCheck -Status "Pass" -Area "Runtime" -Name "OwnerLens package root" -Details $resolvedPackageRoot } else { Add-OwnerLensCheck -Status "Warn" -Area "Runtime" -Name "OwnerLens package root" -Details "Could not find package.json + bin/ownerlens.js. Packaged runtime may still be valid." } $runtimeLayout = $null $nodePath = "" if ($SkipRuntime) { Add-OwnerLensCheck -Status "Info" -Area "Runtime" -Name "Runtime checks" -Details "Skipped by -SkipRuntime." } else { $runtimeLayout = Resolve-OwnerLensRuntimeLayout -ExplicitRuntimePath $RuntimePath -PackageRoot $resolvedPackageRoot -ModuleRoot $moduleRoot if (-not $runtimeLayout) { Add-OwnerLensCheck -Status "Fail" -Area "Runtime" -Name "Runtime layout" -Details "Could not find packaged runtime or source package." -Fix "Run Install-OwnerLensRuntime -Force, pass -RuntimePath, or run from a built OwnerLens repository/package root." } else { $nodePath = Test-OwnerLensRuntimeLayout -Layout $runtimeLayout if ($TestRuntimeStartup) { Test-OwnerLensRuntimeStartup -Layout $runtimeLayout -NodePath $nodePath -DataPath $DataPath -RequestedPort $Port } } } Test-OwnerLensGraph Test-OwnerLensAzure Write-OwnerLensReport if ($FailOnError -and (@($checks | Where-Object Status -eq "Fail").Count -gt 0)) { throw "OwnerLens prerequisite checks failed." } } # SIG # Begin signature block # MII9MQYJKoZIhvcNAQcCoII9IjCCPR4CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDMgu/GIWb6l3Kg # RdIV7Xvd/70/mJupe8IGjfLmhCxolaCCIfYwggXMMIIDtKADAgECAhBUmNLR1FsZ # lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu # dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy # MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv # c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo # b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD # Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH # or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD # PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV # qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70 # ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1 # vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3 # W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK # xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg # trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj # O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7 # swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE # AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ # T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn # LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c # Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF # xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx # AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8 # FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY # lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2 # 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY # uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al # GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk # ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T # 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggaoMIIEkKADAgECAhMzAAJ4V/U8 # qgzRmfYJAAAAAnhXMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ # RCBWZXJpZmllZCBDUyBBT0MgQ0EgMDQwHhcNMjYwNjI1MTI0NTAzWhcNMjYwNjI4 # MTI0NTAzWjBpMQswCQYDVQQGEwJQTDEXMBUGA1UECB4OAUEA8wBkAHoAawBpAGUx # DzANBgNVBAcTBldpZWx1bjEXMBUGA1UEChMOc29mdDRwbSBzcCB6b28xFzAVBgNV # BAMTDnNvZnQ0cG0gc3Agem9vMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKC # AYEAuPBlbn53cMibaGR245K9vL2/yCCQchiHeYTVTY682dov+ITdla7t5ZZbQo/5 # 6UPPdCD5g++gUCL8+w6faHd3ocntt1BWbApXFOLV76InWbsGakogDm4Z/KDMHOKo # IAK3bHCWZU5DRh7fVxvJbgdl1m5Jb1wLJfSakUhBaVHmVVXb1dI3S/UIPzhhwFQe # R3Zqy3jb9ZanZK0FP3/QjMxrzglAknuV2Mj2XKdu/n7oIHhy9p9H9d4HIbJX0zMq # bBpUl6n9AR5tx28Zhtgni6ymlRaPQmz4aqr0a2CevkCSBz23LmlUW9vYK1Bl2jiJ # yQobRIGJHXEuqgUsUnKUxoPwbeY0S2FfCS+VpHUabGxXNZKIS5txyoDI9qrZqRTV # /Stl/ypjTTJ3pUgGjHxLgPUtkY9DhzMb6k17jr0g3J016+97ppmzVgabnbkW45Mb # EmUgkDT7UpS1K9kZZS8L09OVvhKb1XMJyzFRsdrOhR+MoS0nzQkeC4nBwTnAnZTa # RF1BAgMBAAGjggHWMIIB0jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA9 # BgNVHSUENjA0BgorBgEEAYI3YQEABggrBgEFBQcDAwYcKwYBBAGCN2GCycz3dYGA # 0Y0Tg469piSB0sq0aDAdBgNVHQ4EFgQUC3TR4z+u3NeCL6E9kH0m33eJQ14wHwYD # VR0jBBgwFoAUayVB3vtrfP0YgAotf492XapzPbgwZwYDVR0fBGAwXjBcoFqgWIZW # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIw # SUQlMjBWZXJpZmllZCUyMENTJTIwQU9DJTIwQ0ElMjAwNC5jcmwwdAYIKwYBBQUH # AQEEaDBmMGQGCCsGAQUFBzAChlhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp # b3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUyMEFPQyUy # MENBJTIwMDQuY3J0MFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNo # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5o # dG0wDQYJKoZIhvcNAQEMBQADggIBAGgkNRfToOtvheMuLBxF1Cw55qwAacMzpygX # mS+5mKCmQWHZlo7klGkBvExdm4sNofuaCfBLmcJ7XxIvle/RXMLcQp812FH3sJb9 # ZiVT9/getsvUKrrYVPsfU302Rup6Ick4wRq4LEJKKKG89JsRUk8U3OiFCPkyFd8+ # 484tGbFUPrOCUD74ag5x3ehoyVYxq+JmiVjy81iaNKiM4zYU7HFXfMmmGx/FFzRQ # N/ELfO9g9SV4cnwmioncOEU6ZvvZxxTg23D8QIaqEggXxRirMiBdi7WGG+AYGDne # cIpMGzNuCFw8enHyVnR2eD/ClEmSGI4ZMNh01fvefThuUQSjXEEncepTGSvjEVml # MTJEa45qjuB38EFRrjJVFZQnOCzsLXx8HCGdVm8L1zIy99T7FBCG3wy0W3RDb7ow # 1acaYyDzEQM+O79Cag9hzuHJW+nRtAJon2ID1qnkVnH2jnE3EvVu6/SmJV4QjgEX # r7z3j3e6w97diJSPvpjyeI0djOLhy3FJt1tKrtVhN6RAA/586wBxyLyJz5OjxAej # 2rXIxBfa89xKVSmDWLqmc2vdGwzYf3p/A33ocaPIibBg85X4rt17aISsk2VBR4gx # JkWYIdOzIRNHOwYpICbz/GMP5dnytvSXayVz/7hHeaE78L1cw0pjYlaHGVzXDzAS # IqGC9feNMIIGqDCCBJCgAwIBAgITMwACeFf1PKoM0Zn2CQAAAAJ4VzANBgkqhkiG # 9w0BAQwFADBaMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSswKQYDVQQDEyJNaWNyb3NvZnQgSUQgVmVyaWZpZWQgQ1MgQU9DIENB # IDA0MB4XDTI2MDYyNTEyNDUwM1oXDTI2MDYyODEyNDUwM1owaTELMAkGA1UEBhMC # UEwxFzAVBgNVBAgeDgFBAPMAZAB6AGsAaQBlMQ8wDQYDVQQHEwZXaWVsdW4xFzAV # BgNVBAoTDnNvZnQ0cG0gc3Agem9vMRcwFQYDVQQDEw5zb2Z0NHBtIHNwIHpvbzCC # AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALjwZW5+d3DIm2hkduOSvby9 # v8ggkHIYh3mE1U2OvNnaL/iE3ZWu7eWWW0KP+elDz3Qg+YPvoFAi/PsOn2h3d6HJ # 7bdQVmwKVxTi1e+iJ1m7BmpKIA5uGfygzBziqCACt2xwlmVOQ0Ye31cbyW4HZdZu # SW9cCyX0mpFIQWlR5lVV29XSN0v1CD84YcBUHkd2ast42/WWp2StBT9/0IzMa84J # QJJ7ldjI9lynbv5+6CB4cvafR/XeByGyV9MzKmwaVJep/QEebcdvGYbYJ4usppUW # j0Js+Gqq9Gtgnr5Akgc9ty5pVFvb2CtQZdo4ickKG0SBiR1xLqoFLFJylMaD8G3m # NEthXwkvlaR1GmxsVzWSiEubccqAyPaq2akU1f0rZf8qY00yd6VIBox8S4D1LZGP # Q4czG+pNe469INydNevve6aZs1YGm525FuOTGxJlIJA0+1KUtSvZGWUvC9PTlb4S # m9VzCcsxUbHazoUfjKEtJ80JHguJwcE5wJ2U2kRdQQIDAQABo4IB1jCCAdIwDAYD # VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwPQYDVR0lBDYwNAYKKwYBBAGCN2EB # AAYIKwYBBQUHAwMGHCsGAQQBgjdhgsnM93WBgNGNE4OOvaYkgdLKtGgwHQYDVR0O # BBYEFAt00eM/rtzXgi+hPZB9Jt93iUNeMB8GA1UdIwQYMBaAFGslQd77a3z9GIAK # LX+Pdl2qcz24MGcGA1UdHwRgMF4wXKBaoFiGVmh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUy # MEFPQyUyMENBJTIwMDQuY3JsMHQGCCsGAQUFBwEBBGgwZjBkBggrBgEFBQcwAoZY # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQl # MjBJRCUyMFZlcmlmaWVkJTIwQ1MlMjBBT0MlMjBDQSUyMDA0LmNydDBUBgNVHSAE # TTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMA0GCSqGSIb3DQEBDAUAA4IC # AQBoJDUX06Drb4XjLiwcRdQsOeasAGnDM6coF5kvuZigpkFh2ZaO5JRpAbxMXZuL # DaH7mgnwS5nCe18SL5Xv0VzC3EKfNdhR97CW/WYlU/f4HrbL1Cq62FT7H1N9Nkbq # eiHJOMEauCxCSiihvPSbEVJPFNzohQj5MhXfPuPOLRmxVD6zglA++GoOcd3oaMlW # MaviZolY8vNYmjSojOM2FOxxV3zJphsfxRc0UDfxC3zvYPUleHJ8JoqJ3DhFOmb7 # 2ccU4Ntw/ECGqhIIF8UYqzIgXYu1hhvgGBg53nCKTBszbghcPHpx8lZ0dng/wpRJ # khiOGTDYdNX73n04blEEo1xBJ3HqUxkr4xFZpTEyRGuOao7gd/BBUa4yVRWUJzgs # 7C18fBwhnVZvC9cyMvfU+xQQht8MtFt0Q2+6MNWnGmMg8xEDPju/QmoPYc7hyVvp # 0bQCaJ9iA9ap5FZx9o5xNxL1buv0piVeEI4BF6+89493usPe3YiUj76Y8niNHYzi # 4ctxSbdbSq7VYTekQAP+fOsAcci8ic+To8QHo9q1yMQX2vPcSlUpg1i6pnNr3RsM # 2H96fwN96HGjyImwYPOV+K7de2iErJNlQUeIMSZFmCHTsyETRzsGKSAm8/xjD+XZ # 8rb0l2slc/+4R3mhO/C9XMNKY2JWhxlc1w8wEiKhgvX3jTCCBygwggUQoAMCAQIC # EzMAAAAWMZKNkgJle5oAAAAAABYwDQYJKoZIhvcNAQEMBQAwYzELMAkGA1UEBhMC # VVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWlj # cm9zb2Z0IElEIFZlcmlmaWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTAeFw0yNjAz # MjYxODExMjlaFw0zMTAzMjYxODExMjlaMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBW # ZXJpZmllZCBDUyBBT0MgQ0EgMDQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK # AoICAQDKVfrI2+gJMM/0bQ5OVKNdvOASzLbUUMvXuf+Vl7YGuofPaZHVo3gMHF5i # nT+GMSpIcfIZ9qtXU1UG68ry8vNbQtOL4Nm30ifXpqI1+ByiAWLO1YT0WnzG7XPO # uoTeeWsNZv5FmjxCsReBZvyzyzCyXZbu1EQfJxWTH4ebUwtAiW9rqMf9eDj/wYhi # EfNteJV3ZFeibD2ztCHr9JhFdd97XbnCHgQoTIqc02X5xlRKtUGBa++OtHBBjiJ/ # uwBnzTkqu4FjpZjQeJtrmda+ur1CT2jflWIB/ypn7u7V9tvW9wJbJYt/H2EtJ0GO # NWxJZ7TEu8jWPindOO3lzPP7UtzS/mVDV94HucWaltmsra6zSG8BoEJ87IM8QSb7 # vfm/O41FhYkUv89WIj5ES2O4kxyiMSfe95CMivCuYrRP2hKvx7egPMrWgDDBkxML # grKZO9hRNUMm8vk3w5b9SogHOyJVhxyFm8aFXfIxgqDF4S0g4bhbhnzljmSlCLlu # mMZcXFGDjpF2tNoAu3VGFGYtHtTSNVKvZpgB3b4ynaoDkbPf+Wg4523jt4VneasB # gZhC1srZI2NCnCBBfgjLq04pqEKAWEohyW2K29KSkkHvt5VaE1ac3Yt+oyiOzMS5 # 7tXwQDJLGvLg/OXFO0VNvczDndfIfXYExB/ab2PuMSwd5VIBOwIDAQABo4IB3DCC # AdgwDgYDVR0PAQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRr # JUHe+2t8/RiACi1/j3ZdqnM9uDBUBgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9z # aXRvcnkuaHRtMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMBIGA1UdEwEB/wQI # MAYBAf8CAQAwHwYDVR0jBBgwFoAU2UEpsA8PY2zvadf1zSmepEhqMOYwcAYDVR0f # BGkwZzBloGOgYYZfaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwv # TWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENvZGUlMjBTaWduaW5nJTIwUENB # JTIwMjAyMS5jcmwwfQYIKwYBBQUHAQEEcTBvMG0GCCsGAQUFBzAChmFodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIw # VmVyaWZpZWQlMjBDb2RlJTIwU2lnbmluZyUyMFBDQSUyMDIwMjEuY3J0MA0GCSqG # SIb3DQEBDAUAA4ICAQAG1VBeVHTVRBljlcZD3IiMxwPyMjQyLNaEnVu5mODm2hRB # JfH8GsBLATmrHAc8F47jmk5CnpUPiIguCbw6Z/KVj4Dsoiq228NSLMLewFfGMri7 # uwNGLISC5ccp8vUdADDEIsS2dE+QI9OwkDpv3XuUD7d+hAgcLVcMOl1AsfEZtsZe # nhGvSYUrm/FuLq0BqEGL9GXM5c+Ho9q8o+Vn/S+GWQN2y+gkRO15s0kI05nUpq/d # OD4ri9rgVs6tipEd0YZqGgD+CZNiaZWrDTOQbNPncd2F9qOsUa20miYruoT5PwJA # aI+QQiTE2ZJeMJOkOpzhTUgqVMZwZidEUZKCqudaeQA08WwnkQMfKyHzaU8j48UL # cU4hUwvMsv7fSurOe9GAdRQCPvF8WcSK5oDHe8VVJM4tv6KKCm91HqLx9JamBgRI # 6R2SfY3nu26EGznu0rCg/769z8xWm4PVcC2ZaL6VlKVqFp1NsN8YqMyf5t+bbGVb # 09noFKcJG/UwyGlxRmQBlfeBUQx5/ytlzZzsEnhrJF9fTAfje8j3OdX5lEnePTFQ # LRlvzZFBqUXnIeQKv3fHQjC9m2fo/Z01DII/qp3d8LhGVUW0BCG04fRwHJNH8iqq # CG/qofMv+kym2AxBDnHzNgRjL60JOFiBgiurvLhYQNhB95KWojFA6shQnggkMTCC # B54wggWGoAMCAQICEzMAAAAHh6M0o3uljhwAAAAAAAcwDQYJKoZIhvcNAQEMBQAw # dzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjFI # MEYGA1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNhdGlvbiBSb290IENl # cnRpZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIxMDQwMTIwMDUyMFoXDTM2MDQw # MTIwMTUyMFowYzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjE0MDIGA1UEAxMrTWljcm9zb2Z0IElEIFZlcmlmaWVkIENvZGUgU2ln # bmluZyBQQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALLw # wK8ZiCji3VR6TElsaQhVCbRS/3pK+MHrJSj3Zxd3KU3rlfL3qrZilYKJNqztA9OQ # acr1AwoNcHbKBLbsQAhBnIB34zxf52bDpIO3NJlfIaTE/xrweLoQ71lzCHkD7A4A # s1Bs076Iu+mA6cQzsYYH/Cbl1icwQ6C65rU4V9NQhNUwgrx9rGQ//h890Q8JdjLL # w0nV+ayQ2Fbkd242o9kH82RZsH3HEyqjAB5a8+Ae2nPIPc8sZU6ZE7iRrRZywRmr # KDp5+TcmJX9MRff241UaOBs4NmHOyke8oU1TYrkxh+YeHgfWo5tTgkoSMoayqoDp # HOLJs+qG8Tvh8SnifW2Jj3+ii11TS8/FGngEaNAWrbyfNrC69oKpRQXY9bGH6jn9 # NEJv9weFxhTwyvx9OJLXmRGbAUXN1U9nf4lXezky6Uh/cgjkVd6CGUAf0K+Jw+GE # /5VpIVbcNr9rNE50Sbmy/4RTCEGvOq3GhjITbCa4crCzTTHgYYjHs1NbOc6brH+e # KpWLtr+bGecy9CrwQyx7S/BfYJ+ozst7+yZtG2wR461uckFu0t+gCwLdN0A6cFtS # RtR8bvxVFyWwTtgMMFRuBa3vmUOTnfKLsLefRaQcVTgRnzeLzdpt32cdYKp+dhr2 # ogc+qM6K4CBI5/j4VFyC4QFeUP2YAidLtvpXRRo3AgMBAAGjggI1MIICMTAOBgNV # HQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNlBKbAPD2Ns # 72nX9c0pnqRIajDmMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNo # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5o # dG0wGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAf # BgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0wezB5oHeg # dYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0 # JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUl # MjBBdXRob3JpdHklMjAyMDIwLmNybDCBwwYIKwYBBQUHAQEEgbYwgbMwgYEGCCsG # AQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p # Y3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRp # ZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwLQYIKwYBBQUHMAGGIWh0dHA6 # Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDANBgkqhkiG9w0BAQwFAAOCAgEA # fyUqnv7Uq+rdZgrbVyNMul5skONbhls5fccPlmIbzi+OwVdPQ4H55v7VOInnmezQ # EeW4LqK0wja+fBznANbXLB0KrdMCbHQpbLvG6UA/Xv2pfpVIE1CRFfNF4XKO8XYE # a3oW8oVH+KZHgIQRIwAbyFKQ9iyj4aOWeAzwk+f9E5StNp5T8FG7/VEURIVWArbA # zPt9ThVN3w1fAZkF7+YU9kbq1bCR2YD+MtunSQ1Rft6XG7b4e0ejRA7mB2IoX5hN # h3UEauY0byxNRG+fT2MCEhQl9g2i2fs6VOG19CNep7SquKaBjhWmirYyANb0RJSL # WjinMLXNOAga10n8i9jqeprzSMU5ODmrMCJE12xS/NWShg/tuLjAsKP6SzYZ+1Ry # 358ZTFcx0FS/mx2vSoU8s8HRvy+rnXqyUJ9HBqS0DErVLjQwK8VtsBdekBmdTbQV # oCgPCqr+PDPB3xajYnzevs7eidBsM71PINK2BoE2UfMwxCCX3mccFgx6UsQeRSdV # VVNSyALQe6PT12418xon2iDGE81OGCreLzDcMAZnrUAx4XQLUz6ZTl65yPUiOh3k # 7Yww94lDf+8oG2oZmDh5O1Qe38E+M3vhKwmzIeoB1dVLlz4i3IpaDcR+iuGjH2Td # aC1ZOmBXiCRKJLj4DT2uhJ04ji+tHD6n58vhavFIrmcxghqRMIIajQIBATBxMFox # CzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzAp # BgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBBT0MgQ0EgMDQCEzMAAnhX # 9TyqDNGZ9gkAAAACeFcwDQYJYIZIAWUDBAIBBQCgXjAQBgorBgEEAYI3AgEMMQIw # ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAvBgkqhkiG9w0BCQQxIgQgjbUa # MzxbO+qGh3VYQf2ubmqk+sb+/mVtiyFta1wGJKowDQYJKoZIhvcNAQEBBQAEggGA # KuwYjHYurJ4qTwz9q9qVWpxOgOD5RH3Q4TTRG/J+LHIdvO4kgOppYBX5CKkk2RtF # QVlwCT7dDfa6mn9oB88Ty7vdqRfmFg+p94eQsiKC8u9MDBuLvGOMzk/yW5TLznbl # R5wKNnspXreib0C7LShEmOYyLCgxAVUNLwjELJ9WtKyanbGWf/aXRfS5+HZblqIv # r4u+fqimr2BvU/vnP47TcKr8XfJtBJTlM/zefFiPg+Mgw5IO14eedt4NQfiuhz2l # uJhiOgRd4v5AwxvjlTMQb1l9Sydk+84kGQRs/F+pNxlJFz72u29fX5t6LNWI4fhQ # 9duu0WG0+t3O94Jjirw6a4zZJxTKodjoAza0jHXU5k5Of3yczkRACgD/ZbUsCaBc # /bEFSOiEZ65CnvySSZt7eAmhKg4UrT22gXJfC+A6EgICc2lBuzTrQrYQUf7/QXK+ # 3aOUJ162dWf9JOGWiRUmry1oCRPpTESfA32WWEnJmiq136WG7jkIve6z0mW0MEhg # oYIYETCCGA0GCisGAQQBgjcDAwExghf9MIIX+QYJKoZIhvcNAQcCoIIX6jCCF+YC # AQMxDzANBglghkgBZQMEAgEFADCCAWIGCyqGSIb3DQEJEAEEoIIBUQSCAU0wggFJ # AgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIDqKBjvUexIYZdslCdwO # lTfQHzAG6pdk5BsT5L/uOF9vAgZqF2ZCTXYYEzIwMjYwNjI3MTE0ODI1LjgzOFow # BIACAfSggeGkgd4wgdsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNV # BAsTHm5TaGllbGQgVFNTIEVTTjo3QTAwLTA1RTAtRDk0NzE1MDMGA1UEAxMsTWlj # cm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBTdGFtcGluZyBBdXRob3JpdHmggg8hMIIH # gjCCBWqgAwIBAgITMwAAAAXlzw//Zi7JhwAAAAAABTANBgkqhkiG9w0BAQwFADB3 # MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMUgw # RgYDVQQDEz9NaWNyb3NvZnQgSWRlbnRpdHkgVmVyaWZpY2F0aW9uIFJvb3QgQ2Vy # dGlmaWNhdGUgQXV0aG9yaXR5IDIwMjAwHhcNMjAxMTE5MjAzMjMxWhcNMzUxMTE5 # MjA0MjMxWjBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBp # bmcgQ0EgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ5851Jj # /eDFnwV9Y7UGIqMcHtfnlzPREwW9ZUZHd5HBXXBvf7KrQ5cMSqFSHGqg2/qJhYqO # QxwuEQXG8kB41wsDJP5d0zmLYKAY8Zxv3lYkuLDsfMuIEqvGYOPURAH+Ybl4SJEE # Snt0MbPEoKdNihwM5xGv0rGofJ1qOYSTNcc55EbBT7uq3wx3mXhtVmtcCEr5ZKTk # KKE1CxZvNPWdGWJUPC6e4uRfWHIhZcgCsJ+sozf5EeH5KrlFnxpjKKTavwfFP6Xa # GZGWUG8TZaiTogRoAlqcevbiqioUz1Yt4FRK53P6ovnUfANjIgM9JDdJ4e0qiDRm # 5sOTiEQtBLGd9Vhd1MadxoGcHrRCsS5rO9yhv2fjJHrmlQ0EIXmp4DhDBieKUGR+ # eZ4CNE3ctW4uvSDQVeSp9h1SaPV8UWEfyTxgGjOsRpeexIveR1MPTVf7gt8hY64X # NPO6iyUGsEgt8c2PxF87E+CO7A28TpjNq5eLiiunhKbq0XbjkNoU5JhtYUrlmAbp # xRjb9tSreDdtACpm3rkpxp7AQndnI0Shu/fk1/rE3oWsDqMX3jjv40e8KN5YsJBn # czyWB4JyeeFMW3JBfdeAKhzohFe8U5w9WuvcP1E8cIxLoKSDzCCBOu0hWdjzKNu8 # Y5SwB1lt5dQhABYyzR3dxEO/T1K/BVF3rV69AgMBAAGjggIbMIICFzAOBgNVHQ8B # Af8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGtpKDo1L0hjQM97 # 2K9J6T7ZPdshMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNodHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0w # EwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEw # DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2io # ojCBhAYDVR0fBH0wezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br # aW9wcy9jcmwvTWljcm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBS # b290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCBlAYIKwYB # BQUHAQEEgYcwgYQwgYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0 # aW9uJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQw # DQYJKoZIhvcNAQEMBQADggIBAF+Idsd+bbVaFXXnTHho+k7h2ESZJRWluLE0Oa/p # O+4ge/XEizXvhs0Y7+KVYyb4nHlugBesnFqBGEdC2IWmtKMyS1OWIviwpnK3aL5J # edwzbeBF7POyg6IGG/XhhJ3UqWeWTO+Czb1c2NP5zyEh89F72u9UIw+IfvM9lzDm # c2O2END7MPnrcjWdQnrLn1Ntday7JSyrDvBdmgbNnCKNZPmhzoa8PccOiQljjTW6 # GePe5sGFuRHzdFt8y+bN2neF7Zu8hTO1I64XNGqst8S+w+RUdie8fXC1jKu3m9KG # IqF4aldrYBamyh3g4nJPj/LR2CBaLyD+2BuGZCVmoNR/dSpRCxlot0i79dKOChmo # ONqbMI8m04uLaEHAv4qwKHQ1vBzbV/nG89LDKbRSSvijmwJwxRxLLpMQ/u4xXxFf # R4f/gksSkbJp7oqLwliDm/h+w0aJ/U5ccnYhYb7vPKNMN+SZDWycU5ODIRfyoGl5 # 9BsXR/HpRGtiJquOYGmvA/pk5vC1lcnbeMrcWD/26ozePQ/TWfNXKBOmkFpvPE8C # H+EeGGWzqTCjdAsno2jzTeNSxlx3glDGJgcdz5D/AAxw9Sdgq/+rY7jjgs7X6fqP # TXPmaCAJKVHAP19oEjJIBwD1LyHbaEgBxFCogYSOiUIr0Xqcr1nJfiWG2GwYe6Zo # AF1bMIIHlzCCBX+gAwIBAgITMwAAAFhlzes/odf80gAAAAAAWDANBgkqhkiG9w0B # AQwFADBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcg # Q0EgMjAyMDAeFw0yNTEwMjMyMDQ2NTVaFw0yNjEwMjIyMDQ2NTVaMIHbMQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3Nv # ZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046 # N0EwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRp # bWUgU3RhbXBpbmcgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAnXg0pHaQ7PVAlln+HZZrJFcLoKbekhW1yL+QNBUgFFUsjZIKaqqN4oIJ # sJM3ps0rJNSO7ndCNRuZDX2Wgur3Ak77eXrloBXqZmO6ZVXeDNRCLldW4A0/Nfjz # J7XXkdEhjr81ghXEpR7zC+wbaNN+sPSxzLAZBeibDFP7Xws5wX0ZtIsN1a2+Xq5b # vWp3kRMytwskTjunRgeLZL/tBp237JVdRPFAQ9jYRKpCqUBo/v1xjBLRCV3PalKj # nGfb3MN4U7jVyqifFHShcnW5CERRoBmUa6sygDzFSr8e3g93TPNLFUivUE0GmLfb # X5ceD1Gt1FcZ6x/JLVATzk5+BWHbMxwJIVkVPTqSSMjQ6KTKdcnq3pH0c4AFJp/g # lvcpq0U9fzZIjJGGvdpishlRl77RQtUhSjxHvCn3LC/xqQQwOHSQDsGh6NX2D0Rf # sSyEtTAByAae+2w1HByTDTcmlTNLEuQLeCj1gNBdIWj0WOYyDtjjQ/8iTWY6ey1v # b9qHljIj5HgIndT5P9MYk2Vg2e7hKUZNBNbA/hsgBsuoZ+IX89WvjEN9abF91S4O # JVuinmKsLO/MLbnl7ikuD0dN6oA0YewyDQncs12sM9HOtu72QA/TZlefvW8r9xtM # XAYoQlcGjsk8W4Uc7cfqVqbIPjdoc8ZxBzLcXcVyP4p5cyLwvkMCAwEAAaOCAcsw # ggHHMB0GA1UdDgQWBBRyjU3Fer4VxXJ+hjPcRJnxnRIJsDAfBgNVHSMEGDAWgBRr # aSg6NS9IY0DPe9ivSek+2T3bITBsBgNVHR8EZTBjMGGgX6BdhltodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBQdWJsaWMlMjBS # U0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3JsMHkGCCsGAQUFBwEBBG0w # azBpBggrBgEFBQcwAoZdaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j # ZXJ0cy9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBD # QSUyMDIwMjAuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH # AwgwDgYDVR0PAQH/BAQDAgeAMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEw # PwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9j # cy9SZXBvc2l0b3J5Lmh0bTAIBgZngQwBBAIwDQYJKoZIhvcNAQEMBQADggIBAHvr # xIiVF1iHcXvxrJTCD8eOtbPUbK9x+Lz70iYehh+G0UoOMcMf04QD6tQPTeZ5HhGE # Tkcn0raDJ5NpfbRBuKEH31rxbZK97o12KRDNJ3Nu4ePaUIpH/TcWz8PLVOCECywS # xbEgEG20kyydGc46c591tXzpfkJDckjoYrypaerdeQLRQH9LaoTYZfdAzMo+Dy0O # 1DzFJkF5YnsmAM8lt9r1NtXdFjdbFMCbV5dau64mV22s186A8Umi+l239+Ue0cbJ # QIykWhIlhhWhxQgoksqHz7kp2GFZAAeySTmIOQOWyXOA8JA8TISJyn3JDOgStv58 # 3P3V0QSALT6JXDCW26FV208VGJMzkv0S22iOTZJ/oamTpk8RzD8oWT8pfbe1q/k/ # bxPiXYRbzps96a5YOko7n0Vdo61DOJhL/mhk01Y348gq6vhG/VTcdGHh1rCkwOM0 # 5B35AZZq9AtPpfRzJinrHzzGRx+r6fD3ccYMPMMX/Nwd2irzrph172fQcSf1fMwv # wIhmfH4GWJJ+mf1HA6uXoAOVByckguXvlj8gPi7T2ES6RU8+QssfqTNTJKjsBheW # KWv2W4ESVen2L7lCz7i79FhA+0kp0yXJnYwdzWS0ovTINULINmzVyMcSUm5WuVf8 # YZ33cAud2Opr6N1+RuLZDavDvjiehlI5dH+GEy56MYIHQzCCBz8CAQEweDBhMQsw # CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD # VQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAIT # MwAAAFhlzes/odf80gAAAAAAWDANBglghkgBZQMEAgEFAKCCBJwwEQYLKoZIhvcN # AQkQAg8xAgUAMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0B # CQUxDxcNMjYwNjI3MTE0ODI1WjAvBgkqhkiG9w0BCQQxIgQggezmKI5AzpYjgO85 # ssWtBNHNCRfuYQ5AKO0A8yR81V0wgbkGCyqGSIb3DQEJEAIvMYGpMIGmMIGjMIGg # BCDFIlS7sgfQ+wAo1cWbWz+WN69VBds58hbran919aLocTB8MGWkYzBhMQswCQYD # VQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD # EylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAA # AFhlzes/odf80gAAAAAAWDCCA14GCyqGSIb3DQEJEAISMYIDTTCCA0mhggNFMIID # QTCCAikCAQEwggEJoYHhpIHeMIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0EwMC0wNUUwLUQ5NDcxNTAzBgNV # BAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5 # oiMKAQEwBwYFKw4DAhoDFQCdZHkb26ercF2O62vCdZUfUSvEXKBnMGWkYzBhMQsw # CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD # VQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAN # BgkqhkiG9w0BAQsFAAIFAO3qGlEwIhgPMjAyNjA2MjcwOTQ1NTNaGA8yMDI2MDYy # ODA5NDU1M1owdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA7eoaUQIBADAHAgEAAgI6 # 7TAHAgEAAgISWjAKAgUA7etr0QIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEE # AYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IB # AQCTxM6aZLCmh6rJU5n8OPk/c5mpi77Po2rmgp8MF4i6JgcTLHPHZHIMsaRWIC5N # dNKd09gzolu3tpm1clZvkU0pMuTgY17i9gj+x1SirjcXn8S8j3xERiS4DA8oj8Y6 # vq02KszafhjGBuUyfnCVeCcYnK7FfSspCncG4/Z6QniewJN7uz/4IHx1gEiOuM7a # BYDDUAT6xSNNm6a4EItxyElO0J0jAfwbEKU4Vog6jboCxt3XeWPHM43qqptPtt18 # imGs9jsJLHKOL79n7+YfZJcXMCpDo9OsJ8+QM99USnYquhLTtHeCTjCZ1ETTtdi9 # OWxNPA+uzcuOK47CbjeELBaYMA0GCSqGSIb3DQEBAQUABIICAHchneVgtXFnAEcP # E5NAVl9kAVbjPvrJL88NRiKSsOI3nL7XvsD+cQKo2/JqYxw0EXJFUtHhYxS1ghjd # 4X9gEEiws/sI3Ls10zXq21df5G0mZ8otAq15DXNoFBeo+AE9B8DW1Xy4hyKhK094 # TkWj2cYs2Lm8FhsHxfen+4jz4jEKM5ZYgKjHDm0IP2TrerqfPrvQwRHljdtf8sjE # E9fEFby2uwQe7DFv06uu/HQCOb4zO+opkwXiqSkG9CSq7ZoG2ATsPhpAKyt/Ruye # ue9WhJlYBZWIWwh/LnyopFmtDPcQu4aJn++WhYMJAb168HCXh7xQqhC7canOJuJP # YNDl1JVw8xEmS1C0YUgr7fAQdm1ITesiS472WeHyPQ8rEgQNLLexN9IQ4iZvUpO+ # p3Ao6I0gUkIB8sr1Ktto94izGbPglGTPS0285IO3Kqgz8Samwx/J0MzpZby/lPHu # a3LqmQHarSwuSZcl0Zd7FH/U08bxuyxvyhVF6hUpJgl30uuRjhNsR1Mr2/0NR+jr # HKnwy8G0LkAX79VH9Qb5d/bCcaAblUbFIO9On9nQmvb0lhfhLtXaYqtRigLgbWIQ # x/lh+fwDps870jlbGxaaPuWgb44cm36cJ9yNi6ypXrV7AGhW9gWxMk1r/05SkCUT # cA3+D6DfisK5jVq/fcFGSRpjKsaC # SIG # End signature block |