Intune-LAPS.ps1
|
# ========================= Script Parameters ========================= param( [Parameter(HelpMessage = "Client ID of the app registration to use for delegated auth")] [string]$ClientId, [Parameter(HelpMessage = "Tenant ID to use with the specified app registration")] [string]$TenantId ) # Store parameters at script scope for use in authentication functions $script:CustomClientId = $ClientId $script:CustomTenantId = $TenantId # ========================= Version ========================= $script:Version = "1.0.0" # ========================= Cross-Platform Keyboard Shortcuts ========================= $script:IsRunningOnMac = if ($null -ne $IsMacOS) { $IsMacOS } else { $PSVersionTable.OS -match 'Darwin' } # Enable Ctrl+C as input on all platforms (prevents SIGINT, allows capture via ReadKey) [Console]::TreatControlCAsInput = $true Start-Sleep -Milliseconds 100 while ([Console]::KeyAvailable) { $null = [Console]::ReadKey($true) } function Test-QuitShortcut { param([System.ConsoleKeyInfo]$Key) return (($Key.Key -eq 'Q' -and ($Key.Modifiers -band [ConsoleModifiers]::Control)) -or ($Key.Key -eq 'C' -and ($Key.Modifiers -band [ConsoleModifiers]::Control))) } function Get-QuitShortcutText { return "Ctrl+Q Exit" } # ========================= Authentication ========================= $script:MSALAssemblyPaths = @{} function Initialize-MSALAssemblies { <# .SYNOPSIS Loads MSAL assemblies for browser-based authentication. #> $userHome = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME } # Try nuget cache first $nugetPath = Join-Path $userHome ".nuget/packages/microsoft.identity.client" $msalDll = $null $abstractionsDll = $null if (Test-Path $nugetPath) { $latestVersion = Get-ChildItem $nugetPath -Directory | Sort-Object Name -Descending | Select-Object -First 1 if ($latestVersion) { $msalDll = Join-Path $latestVersion.FullName "lib/net6.0/Microsoft.Identity.Client.dll" if (-not (Test-Path $msalDll)) { $msalDll = Join-Path $latestVersion.FullName "lib/netstandard2.0/Microsoft.Identity.Client.dll" } } $abstractionsPath = Join-Path $userHome ".nuget/packages/microsoft.identitymodel.abstractions" if (Test-Path $abstractionsPath) { $latestAbstractions = Get-ChildItem $abstractionsPath -Directory | Sort-Object Name -Descending | Select-Object -First 1 if ($latestAbstractions) { $abstractionsDll = Join-Path $latestAbstractions.FullName "lib/net6.0/Microsoft.IdentityModel.Abstractions.dll" if (-not (Test-Path $abstractionsDll)) { $abstractionsDll = Join-Path $latestAbstractions.FullName "lib/netstandard2.0/Microsoft.IdentityModel.Abstractions.dll" } } } } # Fallback to Az.Accounts if (-not $msalDll -or -not (Test-Path $msalDll)) { $LoadedAzAccountsModule = Get-Module -Name Az.Accounts if ($null -eq $LoadedAzAccountsModule) { $AzAccountsModule = Get-Module -Name Az.Accounts -ListAvailable | Select-Object -First 1 if ($null -eq $AzAccountsModule) { Write-Verbose "Neither nuget cache nor Az.Accounts module found for MSAL" return $false } Import-Module Az.Accounts -ErrorAction SilentlyContinue -Verbose:$false } $LoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() | Select-Object -ExpandProperty Location -ErrorAction SilentlyContinue $AzureCommon = $LoadedAssemblies | Where-Object { $_ -match "[/\\]Modules[/\\]Az.Accounts[/\\]" -and $_ -match "Microsoft.Azure.Common" } if ($AzureCommon) { $AzureCommonLocation = Split-Path -Parent $AzureCommon $foundMsal = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.Identity.Client.dll" -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1 $foundAbstractions = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.IdentityModel.Abstractions.dll" -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1 if ($foundMsal) { $msalDll = $foundMsal.FullName } if ($foundAbstractions) { $abstractionsDll = $foundAbstractions.FullName } } } if (-not $msalDll -or -not (Test-Path $msalDll)) { Write-Verbose "Could not find Microsoft.Identity.Client.dll" return $false } $loadedAssembliesCheck = [System.AppDomain]::CurrentDomain.GetAssemblies() if ($abstractionsDll -and (Test-Path $abstractionsDll)) { $alreadyLoaded = $loadedAssembliesCheck | Where-Object { $_.GetName().Name -eq 'Microsoft.IdentityModel.Abstractions' } | Select-Object -First 1 if (-not $alreadyLoaded) { try { [void][System.Reflection.Assembly]::LoadFrom($abstractionsDll) $script:MSALAssemblyPaths['Microsoft.IdentityModel.Abstractions'] = $abstractionsDll } catch { } } else { $script:MSALAssemblyPaths['Microsoft.IdentityModel.Abstractions'] = $alreadyLoaded.Location } } $alreadyLoaded = $loadedAssembliesCheck | Where-Object { $_.GetName().Name -eq 'Microsoft.Identity.Client' } | Select-Object -First 1 if (-not $alreadyLoaded) { try { [void][System.Reflection.Assembly]::LoadFrom($msalDll) $script:MSALAssemblyPaths['Microsoft.Identity.Client'] = $msalDll } catch { Write-Verbose "Failed to load MSAL: $_" return $false } } else { $script:MSALAssemblyPaths['Microsoft.Identity.Client'] = $alreadyLoaded.Location } return $true } $script:MSALHelperCompiled = $false function Initialize-MSALHelper { <# .SYNOPSIS Pre-compiles the MSAL helper C# code for browser-based authentication. #> if ($script:MSALHelperCompiled) { return $true } $referencedAssemblies = @( $script:MSALAssemblyPaths['Microsoft.IdentityModel.Abstractions'], $script:MSALAssemblyPaths['Microsoft.Identity.Client'] ) | Where-Object { $_ } if ($referencedAssemblies.Count -lt 1) { throw "Missing required MSAL assemblies" } $referencedAssemblies += @("netstandard", "System.Linq", "System.Threading.Tasks", "System.Collections") $code = @" using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; public class LAPSBrowserAuth { public static string GetAccessToken(string clientId, string[] scopes, string tenantId = null) { try { var task = Task.Run(async () => await GetAccessTokenAsync(clientId, scopes, tenantId)); if (task.Wait(TimeSpan.FromSeconds(180))) { return task.Result; } throw new TimeoutException("Authentication timed out"); } catch (AggregateException ae) { if (ae.InnerException != null) throw ae.InnerException; throw; } } private static async Task<string> GetAccessTokenAsync(string clientId, string[] scopes, string tenantId) { var builder = PublicClientApplicationBuilder.Create(clientId) .WithRedirectUri("http://localhost"); if (!string.IsNullOrEmpty(tenantId)) { builder = builder.WithAuthority(string.Format("https://login.microsoftonline.com/{0}", tenantId)); } IPublicClientApplication publicClientApp = builder.Build(); using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(180))) { var webViewOptions = new SystemWebViewOptions { HtmlMessageSuccess = @" <html> <head> <meta charset='UTF-8'> <title>Authentication Successful - LAPS</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #1a5276 0%, #2e86c1 100%); } .container { text-align: center; color: white; } .brand { font-size: 14px; letter-spacing: 4px; margin-bottom: 30px; opacity: 0.9; } .checkmark { font-size: 64px; margin-bottom: 20px; } h1 { margin: 0 0 10px 0; font-weight: 300; font-size: 28px; } p { margin: 0; opacity: 0.9; font-size: 16px; } </style> </head> <body> <div class='container'> <div class='brand'>[ L A P S ]</div> <div class='checkmark'>✓</div> <h1>Authentication Successful</h1> <p>You can close this window and return to PowerShell.</p> </div> </body> </html>", HtmlMessageError = @" <html> <head> <meta charset='UTF-8'> <title>Authentication Failed - LAPS</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); } .container { text-align: center; color: white; } .brand { font-size: 14px; letter-spacing: 4px; margin-bottom: 30px; opacity: 0.9; } .icon { font-size: 64px; margin-bottom: 20px; } h1 { margin: 0 0 10px 0; font-weight: 300; font-size: 28px; } p { margin: 0; opacity: 0.9; font-size: 16px; } </style> </head> <body> <div class='container'> <div class='brand'>[ L A P S ]</div> <div class='icon'>✕</div> <h1>Authentication Failed</h1> <p>Please close this window and try again.</p> </div> </body> </html>" }; var tokenBuilder = publicClientApp.AcquireTokenInteractive(scopes) .WithPrompt(Prompt.SelectAccount) .WithUseEmbeddedWebView(false) .WithSystemWebViewOptions(webViewOptions); if (!string.IsNullOrEmpty(tenantId)) { tokenBuilder = tokenBuilder.WithExtraQueryParameters(string.Format("domain_hint={0}", tenantId)); } var result = await tokenBuilder .ExecuteAsync(cts.Token) .ConfigureAwait(false); return result.AccessToken; } } } "@ try { $null = [LAPSBrowserAuth] $script:MSALHelperCompiled = $true return $true } catch { } Add-Type -ReferencedAssemblies $referencedAssemblies -TypeDefinition $code -Language CSharp -ErrorAction Stop -IgnoreWarnings 3>$null $script:MSALHelperCompiled = $true return $true } function Get-BrowserAccessToken { param( [string[]]$Scopes ) if (-not $script:MSALHelperCompiled) { $null = Initialize-MSALHelper } # Use custom ClientId if provided, otherwise use Microsoft's well-known PowerShell public client ID $clientId = if ($script:CustomClientId) { $script:CustomClientId } else { "14d82eec-204b-4c2f-b7e8-296a70dab67e" } $tenantId = $script:CustomTenantId $scopeArray = $Scopes | ForEach-Object { if ($_ -notlike "https://*") { "https://graph.microsoft.com/$_" } else { $_ } } $accessToken = [LAPSBrowserAuth]::GetAccessToken($clientId, $scopeArray, $tenantId) return $accessToken } # ========================= Graph API Helpers ========================= $script:AccessToken = $null function Connect-LAPSGraph { <# .SYNOPSIS Authenticates to Microsoft Graph with LAPS-required scopes. #> try { if ($script:CustomClientId) { Write-Host "Using custom app registration..." -ForegroundColor Cyan Write-Host " Client ID: $($script:CustomClientId)" -ForegroundColor Gray if ($script:CustomTenantId) { Write-Host " Tenant ID: $($script:CustomTenantId)" -ForegroundColor Gray } } else { Write-Host "Using default Microsoft Graph authentication..." -ForegroundColor Cyan } Write-Host "Opening browser for authentication..." -ForegroundColor Cyan if ($script:MSALHelperCompiled) { Write-Host "Waiting for authentication response..." -ForegroundColor Yellow $script:AccessToken = Get-BrowserAccessToken -Scopes @( "Device.Read.All", "DeviceLocalCredential.Read.All", "DeviceManagementManagedDevices.PrivilegedOperations.All" ) if ($script:AccessToken) { # Decode JWT to verify granted scopes $tokenParts = $script:AccessToken.Split('.') $payload = $tokenParts[1] # Fix base64 padding switch ($payload.Length % 4) { 2 { $payload += '==' } 3 { $payload += '=' } } $decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload)) | ConvertFrom-Json $grantedScopes = $decoded.scp -split ' ' if ($grantedScopes -notcontains 'DeviceLocalCredential.Read.All') { Write-Host "" Write-Host " WARNING: DeviceLocalCredential.Read.All scope was NOT granted." -ForegroundColor Red Write-Host " Your app registration needs this delegated permission with admin consent." -ForegroundColor Yellow Write-Host "" } Write-Host " Connected" -ForegroundColor Green return $true } else { throw "Failed to get access token" } } else { throw "MSAL helper not initialized" } } catch { Write-Host " $($_.Exception.Message)" -ForegroundColor Red return $false } } function Invoke-LAPSGraphRequest { <# .SYNOPSIS Makes an authenticated request to Microsoft Graph API. #> param( [string]$Uri, [string]$Method = "GET", [hashtable]$ExtraHeaders = @{} ) $headers = @{ 'Authorization' = "Bearer $($script:AccessToken)" 'Content-Type' = 'application/json' 'ocp-client-name' = 'LAPS-PowerShell' 'ocp-client-version' = $script:Version } foreach ($key in $ExtraHeaders.Keys) { $headers[$key] = $ExtraHeaders[$key] } try { $response = Invoke-RestMethod -Uri $Uri -Method $Method -Headers $headers -ErrorAction Stop return $response } catch { # PowerShell 7 stores the response body in ErrorDetails.Message $errorDetail = $null $errorCode = $null $statusCode = $null try { $statusCode = $_.Exception.Response.StatusCode.value__ } catch { } try { $errorBody = $_.ErrorDetails.Message | ConvertFrom-Json $errorDetail = $errorBody.error.message $errorCode = $errorBody.error.code } catch { } if ($statusCode -eq 401 -or $statusCode -eq 403) { $msg = "Access denied ($statusCode)" if ($errorDetail) { $msg += ": $errorDetail" } throw $msg } if ($errorDetail) { throw "$errorCode ($statusCode): $errorDetail" } throw $_.Exception.Message } } # ========================= LAPS Functions ========================= function Search-Device { <# .SYNOPSIS Searches for devices in Entra ID by display name. #> param( [string]$SearchTerm ) # Advanced queries on devices require ConsistencyLevel: eventual and $count=true $filter = [System.Uri]::EscapeDataString("startsWith(displayName,'$SearchTerm')") $uri = "https://graph.microsoft.com/v1.0/devices?`$filter=$filter&`$select=id,displayName,operatingSystem,operatingSystemVersion,trustType,accountEnabled&`$count=true&`$top=50&`$orderby=displayName" try { $result = Invoke-LAPSGraphRequest -Uri $uri -ExtraHeaders @{ 'ConsistencyLevel' = 'eventual' } return $result.value } catch { Write-Host " Search failed: $($_.Exception.Message)" -ForegroundColor Red return @() } } function Get-DeviceLAPSCredential { <# .SYNOPSIS Retrieves LAPS credentials for a device using the /directory/ endpoint. Looks up by device name first to get the correct credential info ID. #> param( [string]$DeviceName ) # Step 1: Find the deviceLocalCredentialInfo by device name $filter = [System.Uri]::EscapeDataString("deviceName eq '$DeviceName'") $listUri = "https://graph.microsoft.com/v1.0/directory/deviceLocalCredentials?`$filter=$filter" try { $listResult = Invoke-LAPSGraphRequest -Uri $listUri if (-not $listResult.value -or $listResult.value.Count -eq 0) { throw "No LAPS credentials found for device '$DeviceName'" } $credentialInfoId = $listResult.value[0].id # Step 2: Get the full credential details including passwords $uri = "https://graph.microsoft.com/v1.0/directory/deviceLocalCredentials/${credentialInfoId}?`$select=credentials" $result = Invoke-LAPSGraphRequest -Uri $uri return $result } catch { throw $_.Exception.Message } } function Get-IntuneManagedDeviceId { <# .SYNOPSIS Looks up the Intune managed device ID by device name. #> param( [string]$DeviceName ) $filter = [System.Uri]::EscapeDataString("deviceName eq '$DeviceName'") $uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?`$filter=$filter&`$select=id,deviceName" $result = Invoke-LAPSGraphRequest -Uri $uri if ($result.value -and $result.value.Count -gt 0) { return $result.value[0].id } return $null } function Invoke-LAPSPasswordRotation { <# .SYNOPSIS Triggers an on-demand LAPS password rotation for an Intune-managed device. #> param( [string]$ManagedDeviceId ) # rotateLocalAdminPassword is only available on the beta endpoint $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/${ManagedDeviceId}/rotateLocalAdminPassword" Invoke-LAPSGraphRequest -Uri $uri -Method "POST" } # ========================= TUI Functions ========================= # Dynamic Control Bar System $script:LastControlBarLine = -1 function Show-DynamicControlBar { param( [string]$ControlsText, [switch]$Force ) $currentTop = [Console]::CursorTop # Clear previous control bar if it exists and is at a different line if ($script:LastControlBarLine -ge 0 -and $script:LastControlBarLine -ne ($currentTop + 1)) { try { [Console]::SetCursorPosition(0, $script:LastControlBarLine) Write-Host (" " * [Console]::WindowWidth) -NoNewline } catch { } } $targetTop = $currentTop + 1 # Ensure buffer is tall enough if ($targetTop -ge [Console]::BufferHeight) { [Console]::BufferHeight = $targetTop + 2 } # Draw the control bar below current content try { [Console]::SetCursorPosition(0, $targetTop) Write-Host " $ControlsText" -ForegroundColor DarkGray $script:LastControlBarLine = $targetTop # Always return cursor above the control bar [Console]::SetCursorPosition(0, $currentTop) } catch { } } function Write-LAPSHost { <# .SYNOPSIS Write-Host wrapper that keeps the dynamic control bar below content. #> param( [string]$Object = "", [ConsoleColor]$ForegroundColor, [switch]$NoNewline, [string]$ControlsText = $null ) $params = @{} if ($ForegroundColor) { $params['ForegroundColor'] = $ForegroundColor } if ($NoNewline) { $params['NoNewline'] = $true } Write-Host $Object @params if ($ControlsText) { Show-DynamicControlBar -ControlsText $ControlsText } } function Show-LAPSHeader { Write-Host "[ L A P S ]" -ForegroundColor Cyan -NoNewline Write-Host " v$script:Version" -ForegroundColor DarkGray Write-Host " Local Administrator Password Solution" -ForegroundColor DarkGray Write-Host " with " -ForegroundColor DarkGray -NoNewline Write-Host "PowerShell" -ForegroundColor Blue } function Show-LAPSHeaderMinimal { Write-Host "[ L A P S ]" -ForegroundColor Cyan -NoNewline Write-Host " v$script:Version" -ForegroundColor DarkGray } function Invoke-LAPSExit { param( [string]$Message = "Exiting..." ) [Console]::CursorVisible = $true [Console]::TreatControlCAsInput = $false Clear-Host Show-LAPSHeaderMinimal Write-Host "" # Clear the access token $script:AccessToken = $null Write-Host " Disconnecting from Microsoft Graph..." -ForegroundColor Yellow try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Write-Host " Disconnected from Microsoft Graph." -ForegroundColor Green } catch { Write-Host " Already disconnected from Microsoft Graph." -ForegroundColor DarkGray } Write-Host "" exit 0 } function Test-GlobalShortcut { param( [System.ConsoleKeyInfo]$Key ) if (Test-QuitShortcut -Key $Key) { Invoke-LAPSExit return $true } return $false } function Read-LAPSInput { <# .SYNOPSIS Enhanced input reader with global shortcut handling. #> param( [string]$Prompt, [switch]$Required, [string]$ControlsText ) [Console]::CursorVisible = $true Write-Host "${Prompt}: " -ForegroundColor Cyan -NoNewline # Show control bar below the prompt if ($ControlsText) { $inputLeft = [Console]::CursorLeft $inputTop = [Console]::CursorTop Write-Host "" Write-Host " $ControlsText" -ForegroundColor DarkGray $script:LastControlBarLine = [Console]::CursorTop - 1 [Console]::SetCursorPosition($inputLeft, $inputTop) } $userInput = "" do { $key = [Console]::ReadKey($true) if (Test-GlobalShortcut -Key $key) { return $null } if ($key.Key -eq 'Escape') { Write-Host "" return $null } if ($key.Key -eq 'Enter') { Write-Host "" break } if ($key.Key -eq 'Backspace' -and $userInput.Length -gt 0) { $userInput = $userInput.Substring(0, $userInput.Length - 1) Write-Host "`b `b" -NoNewline } elseif ($key.KeyChar -ne "`0" -and [char]::IsControl($key.KeyChar) -eq $false) { $userInput += $key.KeyChar Write-Host $key.KeyChar -NoNewline -ForegroundColor White } } while ($true) if ($Required -and [string]::IsNullOrWhiteSpace($userInput)) { Write-Host "Input is required." -ForegroundColor DarkRed return Read-LAPSInput -Prompt $Prompt -Required:$Required } return $userInput } function Show-SimpleMenu { <# .SYNOPSIS Arrow-key driven single-selection menu. #> param( [array]$Items, [string]$Title = "Select an option", [int]$DefaultSelection = 0 ) if ($Items.Count -eq 0) { Write-Host "No items to select from." -ForegroundColor Red return -1 } $currentIndex = $DefaultSelection [Console]::CursorVisible = $false try { do { Clear-Host Show-LAPSHeader Write-Host "" Write-Host $Title -ForegroundColor Cyan Write-Host "" for ($i = 0; $i -lt $Items.Count; $i++) { $arrow = if ($i -eq $currentIndex) { ">" } else { " " } $color = if ($i -eq $currentIndex) { "Yellow" } else { "White" } Write-Host " $arrow $($Items[$i])" -ForegroundColor $color } Write-Host "" Write-Host " Up/Down Navigate | ENTER Select | $(Get-QuitShortcutText)" -ForegroundColor DarkGray $key = [Console]::ReadKey($true) if (Test-GlobalShortcut -Key $key) { return -1 } switch ($key.Key) { "UpArrow" { $currentIndex = if ($currentIndex -gt 0) { $currentIndex - 1 } else { $Items.Count - 1 } } "DownArrow" { $currentIndex = if ($currentIndex -lt ($Items.Count - 1)) { $currentIndex + 1 } else { 0 } } "Enter" { return $currentIndex } "Escape" { return -1 } } } while ($true) } finally { [Console]::CursorVisible = $true } } function Show-DeviceSelectionMenu { <# .SYNOPSIS Displays device search results in a selectable menu. #> param( [array]$Devices ) $currentIndex = 0 [Console]::CursorVisible = $false try { do { Clear-Host Show-LAPSHeaderMinimal Write-Host "" Write-Host " Select a device ($($Devices.Count) found)" -ForegroundColor Cyan Write-Host "" for ($i = 0; $i -lt $Devices.Count; $i++) { $device = $Devices[$i] $arrow = if ($i -eq $currentIndex) { ">" } else { " " } $enabled = if ($device.accountEnabled) { "Enabled" } else { "Disabled" } $enabledColor = if ($device.accountEnabled) { "Green" } else { "Red" } $trust = if ($device.trustType) { $device.trustType } else { "Unknown" } $os = if ($device.operatingSystem) { "$($device.operatingSystem) $($device.operatingSystemVersion)" } else { "Unknown OS" } if ($i -eq $currentIndex) { Write-Host " $arrow " -NoNewline -ForegroundColor Yellow Write-Host "$($device.displayName)" -NoNewline -ForegroundColor Yellow Write-Host " $os" -NoNewline -ForegroundColor DarkGray Write-Host " $trust" -NoNewline -ForegroundColor DarkGray Write-Host " $enabled" -ForegroundColor $enabledColor } else { Write-Host " $arrow " -NoNewline Write-Host "$($device.displayName)" -NoNewline -ForegroundColor White Write-Host " $os" -NoNewline -ForegroundColor DarkGray Write-Host " $trust" -NoNewline -ForegroundColor DarkGray Write-Host " $enabled" -ForegroundColor $enabledColor } } Write-Host "" Write-Host " Up/Down Navigate | ENTER Select | ESC Back | $(Get-QuitShortcutText)" -ForegroundColor DarkGray $key = [Console]::ReadKey($true) if (Test-GlobalShortcut -Key $key) { return $null } switch ($key.Key) { "UpArrow" { $currentIndex = if ($currentIndex -gt 0) { $currentIndex - 1 } else { $Devices.Count - 1 } } "DownArrow" { $currentIndex = if ($currentIndex -lt ($Devices.Count - 1)) { $currentIndex + 1 } else { 0 } } "Enter" { return $Devices[$currentIndex] } "Escape" { return $null } } } while ($true) } finally { [Console]::CursorVisible = $true } } function Show-PasswordResult { <# .SYNOPSIS Displays the LAPS password result with copy-to-clipboard option. #> param( [object]$Device, [object]$Credential ) $controlsText = "Ctrl+C Copy | R Rotate | S New Search | Ctrl+Q Exit" $noCredsControlsText = "S New Search | Ctrl+Q Exit" # Decode credential data once outside the loop $password = $null $accountName = $null $backupTime = $null $hasCreds = $false if ($Credential.credentials -and $Credential.credentials.Count -gt 0) { $hasCreds = $true $latestCred = $Credential.credentials | Sort-Object { [datetime]$_.backupDateTime } -Descending | Select-Object -First 1 $accountName = $latestCred.accountName $passwordRaw = $latestCred.passwordBase64 $password = if ($passwordRaw) { try { $bytes = [System.Convert]::FromBase64String($passwordRaw) $utf8 = [System.Text.Encoding]::UTF8.GetString($bytes) if ($utf8 -match '[^\x20-\x7E]') { [System.Text.Encoding]::ASCII.GetString($bytes) } else { $utf8 } } catch { $passwordRaw } } else { "N/A" } $backupTime = if ($latestCred.backupDateTime) { try { ([datetime]$latestCred.backupDateTime).ToString("yyyy-MM-dd h:mm:ss tt") } catch { $latestCred.backupDateTime } } else { "N/A" } } do { Clear-Host $script:LastControlBarLine = -1 Show-LAPSHeaderMinimal Write-Host "" Write-Host " LAPS Credential Retrieved" -ForegroundColor Green Write-Host " $("=" * 40)" -ForegroundColor DarkGray Write-Host "" Write-Host " Device: " -NoNewline -ForegroundColor Gray Write-Host "$($Device.displayName)" -ForegroundColor White if ($hasCreds) { Write-Host "" Write-Host " Account: " -NoNewline -ForegroundColor Gray Write-Host "$accountName" -ForegroundColor Yellow Write-Host " Password: " -NoNewline -ForegroundColor Gray Write-Host "$password" -ForegroundColor Green Write-Host "" Write-Host " Last Rotated: " -NoNewline -ForegroundColor Gray Write-Host "$backupTime" -ForegroundColor White Write-Host "" Write-Host " $("-" * 40)" -ForegroundColor DarkGray # Remember where content ends so we can write below it $script:ContentLine = [Console]::CursorTop # Show dynamic control bar Show-DynamicControlBar -ControlsText $controlsText # Position cursor back at content line for any action output [Console]::SetCursorPosition(0, $script:ContentLine) $key = [Console]::ReadKey($true) # On password screen, Ctrl+C copies instead of quitting if ($key.Key -eq 'C' -and ($key.Modifiers -band [ConsoleModifiers]::Control)) { try { Set-Clipboard -Value $password Write-Host " Password copied to clipboard!" -ForegroundColor Green Show-DynamicControlBar -ControlsText $controlsText Start-Sleep -Seconds 1 } catch { Write-Host " Failed to copy: $_" -ForegroundColor Red Show-DynamicControlBar -ControlsText $controlsText Start-Sleep -Seconds 2 } } elseif ($key.Key -eq 'R') { Write-Host " Rotate password for $($Device.displayName)? (Y/N): " -ForegroundColor Yellow -NoNewline # Save cursor position at end of Y/N prompt $ynLeft = [Console]::CursorLeft $ynTop = [Console]::CursorTop Write-Host "" # blank line between prompt and control bar Show-DynamicControlBar -ControlsText $controlsText # Restore cursor to right after "(Y/N): " so input appears inline [Console]::SetCursorPosition($ynLeft, $ynTop) $confirmKey = [Console]::ReadKey($true) if ($confirmKey.Key -eq 'Y') { Write-Host "Y" Write-Host " Requesting password rotation..." -ForegroundColor Gray try { $managedDeviceId = Get-IntuneManagedDeviceId -DeviceName $Device.displayName if (-not $managedDeviceId) { Write-Host " Device not found in Intune. Rotation requires an Intune-managed device." -ForegroundColor Red Show-DynamicControlBar -ControlsText $controlsText Start-Sleep -Seconds 3 } else { Invoke-LAPSPasswordRotation -ManagedDeviceId $managedDeviceId Write-Host " Password rotation initiated!" -ForegroundColor Green Write-Host " The new password will appear after the device checks in." -ForegroundColor DarkGray Show-DynamicControlBar -ControlsText $controlsText Start-Sleep -Seconds 3 } } catch { Write-Host " Rotation failed: $($_.Exception.Message)" -ForegroundColor Red Show-DynamicControlBar -ControlsText $controlsText Start-Sleep -Seconds 3 } } else { Write-Host "N" } } elseif ($key.Key -eq 'Q' -and ($key.Modifiers -band [ConsoleModifiers]::Control)) { return 'Quit' } elseif ($key.Key -eq 'S') { return 'Search' } elseif ($key.Key -eq 'Escape') { return 'Search' } } else { Write-Host "" Write-Host " No LAPS credentials found for this device." -ForegroundColor Yellow Write-Host " The device may not have LAPS configured or credentials have not been backed up." -ForegroundColor DarkGray Show-DynamicControlBar -ControlsText $noCredsControlsText $key = [Console]::ReadKey($true) if (Test-GlobalShortcut -Key $key) { return 'Quit' } if ($key.Key -eq 'S' -or $key.Key -eq 'Escape' -or $key.Key -eq 'Enter') { return 'Search' } } } while ($true) } # ========================= Prerequisite Checks ========================= function Test-Prerequisites { <# .SYNOPSIS Checks and installs required dependencies. #> $requiresInstall = $false # Check for Microsoft.Graph.Authentication (needed for device queries via SDK fallback) # We use direct REST calls, so we mainly need MSAL Write-Host "Checking prerequisites..." -ForegroundColor Gray # Initialize MSAL assemblies $msalReady = Initialize-MSALAssemblies if (-not $msalReady) { Write-Host " MSAL assemblies not found. Attempting to install Az.Accounts..." -ForegroundColor Yellow try { if (Get-Command Install-PSResource -ErrorAction SilentlyContinue) { Install-PSResource -Name Az.Accounts -Scope CurrentUser -TrustRepository -Confirm:$false -ErrorAction Stop } else { Install-Module -Name Az.Accounts -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop } Import-Module Az.Accounts -ErrorAction SilentlyContinue -Verbose:$false $msalReady = Initialize-MSALAssemblies } catch { Write-Host " Failed to install Az.Accounts: $_" -ForegroundColor Red return $false } } if (-not $msalReady) { Write-Host " Could not load MSAL authentication libraries." -ForegroundColor Red Write-Host " Please install Az.Accounts: Install-Module Az.Accounts -Scope CurrentUser" -ForegroundColor Yellow return $false } # Pre-compile MSAL helper try { $null = Initialize-MSALHelper Write-Host " Prerequisites OK" -ForegroundColor Green } catch { Write-Host " Failed to initialize authentication: $_" -ForegroundColor Red return $false } return $true } # ========================= Main Application Loop ========================= function Start-LAPSApp { <# .SYNOPSIS Main application entry point - runs the LAPS TUI loop. #> Clear-Host Show-LAPSHeader Write-Host "" # Check prerequisites if (-not (Test-Prerequisites)) { Write-Host "" Write-Host "Press Enter to exit" -ForegroundColor Gray $null = [Console]::ReadLine() return } Write-Host "" # Authenticate $connected = Connect-LAPSGraph if (-not $connected) { Write-Host "" Write-Host "Authentication failed. Press Enter to exit" -ForegroundColor Red $null = [Console]::ReadLine() return } # Main search loop while ($true) { Clear-Host Show-LAPSHeaderMinimal Write-Host "" $searchTerm = Read-LAPSInput -Prompt " Search device name" -Required -ControlsText "ESC Back | Ctrl+Q Exit" # Clear the control bar left behind by Read-LAPSInput if ($script:LastControlBarLine -ge 0) { try { [Console]::SetCursorPosition(0, $script:LastControlBarLine) Write-Host (" " * [Console]::WindowWidth) -NoNewline [Console]::SetCursorPosition(0, $script:LastControlBarLine) $script:LastControlBarLine = -1 } catch { } } if ($null -eq $searchTerm) { Invoke-LAPSExit return } # Empty input (just Enter) - treat as back/exit if ([string]::IsNullOrWhiteSpace($searchTerm)) { Invoke-LAPSExit return } # Search for devices Write-Host " Searching..." -ForegroundColor Gray $devices = Search-Device -SearchTerm $searchTerm if ($devices.Count -eq 0) { Write-Host " No devices found matching '$searchTerm'" -ForegroundColor Yellow Write-Host "" Write-Host " Press any key to search again..." -ForegroundColor DarkGray $key = [Console]::ReadKey($true) if (Test-GlobalShortcut -Key $key) { return } continue } # Show device selection menu $selectedDevice = Show-DeviceSelectionMenu -Devices $devices if ($null -eq $selectedDevice) { continue } # Retrieve LAPS credential Clear-Host Show-LAPSHeaderMinimal Write-Host "" Write-Host " Retrieving LAPS credential for $($selectedDevice.displayName)..." -ForegroundColor Gray try { $credential = Get-DeviceLAPSCredential -DeviceName $selectedDevice.displayName $action = Show-PasswordResult -Device $selectedDevice -Credential $credential if ($action -eq 'Quit') { Invoke-LAPSExit; return } # 'Search' continues the loop } catch { Write-Host "" Write-Host " Failed to retrieve LAPS credential: $($_.Exception.Message)" -ForegroundColor Red Write-Host "" Write-Host " This could mean:" -ForegroundColor DarkGray Write-Host " - LAPS is not configured for this device" -ForegroundColor DarkGray Write-Host " - You lack DeviceLocalCredential.Read.All permission" -ForegroundColor DarkGray Write-Host " - The device has not backed up credentials yet" -ForegroundColor DarkGray Write-Host "" Write-Host " Press any key to search again..." -ForegroundColor DarkGray $key = [Console]::ReadKey($true) if (Test-GlobalShortcut -Key $key) { return } } } } # ========================= Entry Point ========================= Start-LAPSApp # SIG # Begin signature block # MIIsCQYJKoZIhvcNAQcCoIIr+jCCK/YCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDNegtycTnnNfRN # Heo1Updn2bIKWVsPHLs71enIPBzJoqCCJRowggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO # +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi # BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw # MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp # Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb # hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT # cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb # NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP # XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr # VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP # ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv # kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM # cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls # 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR # a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6 # MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E # BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg # v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT # SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh # 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw # uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO # QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD # TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G # CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp # bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290 # IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT # AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g # UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB # jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe # AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV # oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk # 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh # P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41 # aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ # ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/ # 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g # Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA # QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B # Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD # AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG # Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n # Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v # Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu # cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG # 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW # 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh # rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp # mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc # ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE # Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz # NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw # 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6 # E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6 # xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt # g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggZL # MIIEs6ADAgECAhEAh4S8tN9yByR3E9jATIZw9DANBgkqhkiG9w0BAQwFADBUMQsw # CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJT # ZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgUjM2MB4XDTI2MDIyNDAwMDAw # MFoXDTI3MDIyNDIzNTk1OVowRDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkthbnNh # czERMA8GA1UECgwITWFyayBPcnIxETAPBgNVBAMMCE1hcmsgT3JyMIICIjANBgkq # hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx9tr2sjXvlV3KjWWeg0HYTDicFwZDZv2 # tI//RO1C9IL7uShmYN0eSeyWZW/GNy7fTOlIJ6poUe4R3/ApsNsw9hpOMXc92Bny # Ds/UXHMYx2YdOO4XI35IxfhZnZhgIj2acQ0BZ542hmYAwtz8c1Xu9xH51eTArmFW # HV8angRsuFMVyKQOraWQs37tqOVwXeH3FQIT0mFBTbmENhgyxAGLq8nZMFM+JqVV # WeRgvTFO48UZf0BhgH84k2M44CcA9vVML7w4yueg6qD6D/k7Opy1OfCR1qxSXI0w # ZeUXodJvgisDRScKZJfPID6PIxxvoeem4VKkV0y3eBF+UtdQ8+NZ7qmlRl2hE6H6 # efWSRNW2imxeVSg9FgQONnJYhkyJmaio/NnLyDB6PyoCDZQaYDiMRRiycHPbYvba # s0THWB2NFsgr3h3QZxQfZnNB2F/ZVdNlfbGpxTK53Yhf5XT0iaEat9r82wwjlP9c # /PEl1q8G53Pco/ykqBk/V2PfohhuwiXBHb5zL518lCPPZmOCdIqyvkgAUzWymHSi # Twm/ZNTNEaHLaktfBJ52G03r7F1YHSxPDJpH84RrBQwNWA8olog3uvvWTWImDuQd # 8PdvhOrluh11pvMWRn+ic6e2E7A4KQr0x4bZoL/gWBTE9tL8AuCJyjxsjiDAbJRx # d3Di5Bi7pGsCAwEAAaOCAaYwggGiMB8GA1UdIwQYMBaAFA8qyyCHKLjsb0iuK1Sm # KaoXpM0MMB0GA1UdDgQWBBRlBYoMei+jtIKM2eL9y3kX+l6hqzAOBgNVHQ8BAf8E # BAMCB4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAzBKBgNVHSAE # QzBBMDUGDCsGAQQBsjEBAgEDAjAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3Rp # Z28uY29tL0NQUzAIBgZngQwBBAEwSQYDVR0fBEIwQDA+oDygOoY4aHR0cDovL2Ny # bC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5jcmww # eQYIKwYBBQUHAQEEbTBrMEQGCCsGAQUFBzAChjhodHRwOi8vY3J0LnNlY3RpZ28u # Y29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ0NBUjM2LmNydDAjBggrBgEFBQcw # AYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wGwYDVR0RBBQwEoEQbW9yckBvcnIz # NjUudGVjaDANBgkqhkiG9w0BAQwFAAOCAYEAQYDywuGVM9hgCjKW/Til/gPycxB1 # XL4OH7/9jV72/HPbBKnwXwiFlgTO+Lo4UEbZNy+WQk60u0XtrBIKUbhlapRGQPrl # 2OKpf9rYOyysg1puVTqnaxY9vevhgB4NVpHqYMi8+Kzpa2rXzXyrVdbVNIMn00ZA # V6tBTr0fhMt3P4oxF0WYQ/GjfUa1/8O3uqeni36iMyCqP7ao9rJgCOgNvEBokRhh # 7fFC5YVIjMKwvU/7CgbkgjIBHfX4UMxU2BNvCGTR2ZA5IznmLsRI/4MEP9LMLV8D # Qm8wh2P1uCaGANSLQ0EQIZtMEm1i03zBwDOTBLVAo7p+2Pw2q7LEOQni6LeX5AzT # nRvHwcisRM3Kpvx+H6wJnL6x7TXZ7YCHhJ4ZTuMWblXJjVKPueEQfIh04x7oVbIV # 8LNqVyoP9gJZfkmn5IW8cwIFAzFMsNqW1URfArzJ5An9xIYCUJbzohgtE71NjqiZ # PI1k4GxzsyeqTNaXEXnzZEfogAvEmHFMMNXGMIIGtDCCBJygAwIBAgIQDcesVwX/ # IZkuQEMiDDpJhjANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UE # ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD # VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcN # MzgwMTE0MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs # IEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5n # IFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEAtHgx0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oR # jzUXMmxCqvkbsDpz4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+Qd # SKWM06qchUP+AbdJgMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRu # QL37QXbDhAktVJMQbzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0 # Xm+nt5pnYJU3Gmq6bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQV # ESYOszFI2Wv82wnJRfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2 # qHxJ0ucS638ZxqU14lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF # 0LJAQQZxst7VvwDDjAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgx # CZSKi17yVp2NL+cnT6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9X # r/u6bDTnYCTKIsDq1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7O # gWmnhFr4yUozZtqgPrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOC # AV0wggFZMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esri # kFb2L9RJ7MtOMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1Ud # DwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkw # JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcw # AoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJv # b3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwB # BAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEw # vb4LyLU0pn/N0IfFiBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8 # G0iP5kvN2n7Jd2E4/iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40 # y8S4F3/a+Z1jEMK/DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCD # A/JYsq7pGdogP8HRtrYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADV # ZKON/gnZruMvNYY2o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4E # Wj7PtspIHBldNE2K9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpV # fHIqQ6Ku/qjTY6hc3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0 # c1ugMZyZZd/BdHLiRu7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7Oi # gizwJWeukcyIPbAvjSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2 # rtY/9TCA6TD8dC3JE3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz # 0scmbKvFoW2jNrbM1pD2T7m3XDCCBu0wggTVoAMCAQICEAqA7xhLjfEFgtHEdqeV # dGgwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD # ZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFt # cGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1IENBMTAeFw0yNTA2MDQwMDAwMDBaFw0z # NjA5MDMyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgU0hBMjU2IFJTQTQwOTYgVGltZXN0YW1w # IFJlc3BvbmRlciAyMDI1IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDQRqwtEsae0OquYFazK1e6b1H/hnAKAd/KN8wZQjBjMqiZ3xTWcfsLwOvRxUwX # cGx8AUjni6bz52fGTfr6PHRNv6T7zsf1Y/E3IU8kgNkeECqVQ+3bzWYesFtkepEr # vUSbf+EIYLkrLKd6qJnuzK8Vcn0DvbDMemQFoxQ2Dsw4vEjoT1FpS54dNApZfKY6 # 1HAldytxNM89PZXUP/5wWWURK+IfxiOg8W9lKMqzdIo7VA1R0V3Zp3DjjANwqAf4 # lEkTlCDQ0/fKJLKLkzGBTpx6EYevvOi7XOc4zyh1uSqgr6UnbksIcFJqLbkIXIPb # cNmA98Oskkkrvt6lPAw/p4oDSRZreiwB7x9ykrjS6GS3NR39iTTFS+ENTqW8m6TH # uOmHHjQNC3zbJ6nJ6SXiLSvw4Smz8U07hqF+8CTXaETkVWz0dVVZw7knh1WZXOLH # gDvundrAtuvz0D3T+dYaNcwafsVCGZKUhQPL1naFKBy1p6llN3QgshRta6Eq4B40 # h5avMcpi54wm0i2ePZD5pPIssoszQyF4//3DoK2O65Uck5Wggn8O2klETsJ7u8xE # ehGifgJYi+6I03UuT1j7FnrqVrOzaQoVJOeeStPeldYRNMmSF3voIgMFtNGh86w3 # ISHNm0IaadCKCkUe2LnwJKa8TIlwCUNVwppwn4D3/Pt5pwIDAQABo4IBlTCCAZEw # DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU5Dv88jHt/f3X85FxYxlQQ89hjOgwHwYD # VR0jBBgwFoAU729TSunkBnx6yuKQVvYv1Ensy04wDgYDVR0PAQH/BAQDAgeAMBYG # A1UdJQEB/wQMMAoGCCsGAQUFBwMIMIGVBggrBgEFBQcBAQSBiDCBhTAkBggrBgEF # BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMF0GCCsGAQUFBzAChlFodHRw # Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3Rh # bXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcnQwXwYDVR0fBFgwVjBUoFKgUIZO # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0 # YW1waW5nUlNBNDA5NlNIQTI1NjIwMjVDQTEuY3JsMCAGA1UdIAQZMBcwCAYGZ4EM # AQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAZSqt8RwnBLmuYEHs # 0QhEnmNAciH45PYiT9s1i6UKtW+FERp8FgXRGQ/YAavXzWjZhY+hIfP2JkQ38U+w # tJPBVBajYfrbIYG+Dui4I4PCvHpQuPqFgqp1PzC/ZRX4pvP/ciZmUnthfAEP1HSh # TrY+2DE5qjzvZs7JIIgt0GCFD9ktx0LxxtRQ7vllKluHWiKk6FxRPyUPxAAYH2Vy # 1lNM4kzekd8oEARzFAWgeW3az2xejEWLNN4eKGxDJ8WDl/FQUSntbjZ80FU3i54t # px5F/0Kr15zW/mJAxZMVBrTE2oi0fcI8VMbtoRAmaaslNXdCG1+lqvP4FbrQ6IwS # BXkZagHLhFU9HCrG/syTRLLhAezu/3Lr00GrJzPQFnCEH1Y58678IgmfORBPC1JK # kYaEt2OdDh4GmO0/5cHelAK2/gTlQJINqDr6JfwyYHXSd+V08X1JUPvB4ILfJdmL # +66Gp3CSBXG6IwXMZUXBhtCyIaehr0XkBoDIGMUG1dUtwq1qmcwbdUfcSYCn+Own # cVUXf53VJUNOaMWMts0VlRYxe5nK+At+DI96HAlXHAL5SlfYxJ7La54i71McVWRP # 66bW+yERNpbJCjyCYG2j+bdpxo/1Cy4uPcU3AWVPGrbn5PhDBf3Froguzzhk++am # i+r3Qrx5bIbY3TVzgiFI7Gq3zWcxggZFMIIGQQIBATBpMFQxCzAJBgNVBAYTAkdC # MRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVi # bGljIENvZGUgU2lnbmluZyBDQSBSMzYCEQCHhLy033IHJHcT2MBMhnD0MA0GCWCG # SAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcN # AQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUw # LwYJKoZIhvcNAQkEMSIEICaSSstCjDDoIXXWuB7Zpw1t7A6u7JjIA/6PbDfin6+P # MA0GCSqGSIb3DQEBAQUABIICAAx5w6F0sH0QQtR2gEPHgIAL98Up8TlKw5BwApHJ # 3xUeYWNJA5IIWEZDk5OFdf1LMyLRWAq/BWjpvEw0eDBQHnmkJWoz5w1pBib/a01l # fayahYHgHrBUYlaz2vFr1DmicptQfezrc9ITgz+sCVp5eG5+LP/MGwA6wd7IJ3wd # 8bPxO4jZ5GaKd6q7XNFG8FtTHZP7zXACg28trkS06cMBO0XJWEqJDlurN99Fxib1 # 0td6wweOuAFo9IVam6IPQ5vnLuqrtUY+Bgrl+4AlOWuxjBioRJ8dDKKgzi9rhBSe # sB8+rH57GfE1VY72MXTIEXmZOPje+8mu37PwxSAfnkYyBPnOns2E7AOEif+zIDsV # KBuvZHC7Mg7TXLoDdDLCbN7xfqmIah1ZVQ/8ihUHXbsfV0H+59anDynBna2bs6Kw # VzfwZya9PcrKf7icSwn82nGS/vn4tHYVo2ponzb+0MaaFtjPJzg0u78mkDA1MhjV # z+AgeFiQ+Dlm1wcDrtghP1F+i7d6A/R7RDSUW6Lb/p4u4ov2HmsfvO6CcgEpgevG # 7IILHf+p8UR5tI+n4A8hRPYx/YUoHSQ56PXuBArazLMgiLBf5ScXjJg1KEjAIphI # LtH5iU8Eksfja0PM9wHSIkPuDNihslWqliIizfiSWbzu68mp50MoxyhylJRlGL/1 # 120/oYIDJjCCAyIGCSqGSIb3DQEJBjGCAxMwggMPAgEBMH0waTELMAkGA1UEBhMC # VVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBU # cnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2IFNIQTI1NiAyMDI1IENBMQIQ # CoDvGEuN8QWC0cR2p5V0aDANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzEL # BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI2MDQxNDA4NTAzNVowLwYJKoZI # hvcNAQkEMSIEIH2fEXM2BxTVrWrYEchTSp7+dxkV61wkhR7uEFwxdc9NMA0GCSqG # SIb3DQEBAQUABIICAH5afKlR6xTE8x1+aHR6c0i8mD8d+/du8l9TMRkvhpNWD05k # ix9ureGgeR43GFbkS1HFDLfzkhVhuj8xlPJ1c1Kehv+q8ZDNlLE3XLW/32vUVA8x # XCRCB5sQMKcxyPy4eGyoU6kw3I/DRiBHPrqviRAXb5G/zz/ifZX4+IKOEqY/qvI7 # /5ifA2Z3/Pnzi/84KyNUXeZgE4ab94JVVy8tWMXmbjoZsiF2eHtrGOdmW5J/N2mq # p5CXxrEVIlPRuylXjnXbtCUYM1fzBAvpdHdnMERiOHKr4o9NCc6A9RJLjtpeYPc+ # CDY6wHKExbdC4hZIh5R6ud8bJUkMXwmUTjU49MtVBsKST9+ilgfjqaWWt9ARBJyw # dUm6cuHpb12SFWhYHudzAZFxPjNcICDZx/EcZcatReekoPxdcQBNIVpyM+ntPZO5 # Wr0FkaH1tBe3rlhO/9eM57hwfBKsLcII0mchIr3OSgqFuY/qJYFXoQhe17FdnImV # DbO00hbw2EFBydlk7RUsWPEcLCCfpGJUzftlN4nDtCM64QePSI0GXtGLpF8g7fEe # gt3A/t1/TwGSKvZHSx+OwW/89r43VsUiv55aH5/QUZnONuS8KmEpmzWhKewnd55y # IcTmsH6eD72FefVPWbKOWSOaXEFneYCiOxSxSrDdyVTFiroXbcTouMLHyC7f # SIG # End signature block |