Public/Get-GkLapsPassword.ps1
|
function Get-GkLapsPassword { <# .SYNOPSIS Retrieve the Windows LAPS local administrator password for a device. .DESCRIPTION Reads GET /directory/deviceLocalCredentials/{deviceId}?$select=credentials, which returns the Windows LAPS-managed local administrator account and its password, plus any previous credentials still within the backup window. Without -DeviceId the cmdlet instead lists which devices have LAPS credentials at all — useful for confirming coverage. Graph enforces the split, and so do the scopes: the list endpoint documents DeviceLocalCredential.ReadBasic.All and deliberately excludes passwords, while retrieving one requires DeviceLocalCredential.Read.All. Only the scope for the mode you are using is validated. The password is returned as plain text on the result but is NOT shown by the default view, so it does not splash across the screen or into a transcript. Capture it deliberately with Select-Object Password. Reading a LAPS password is an audited, privileged action, and the password should be rotated afterwards. .PARAMETER DeviceId One or more device IDs (the Entra deviceId, not the object id). Accepts pipeline input by the DeviceId property, so Get-GkDeviceInventory output can be piped in. Omit to list devices that have credentials, without retrieving any password. .PARAMETER IncludePrevious Also return credentials older than the current one, where the backup window still holds them. Off by default: the current password is almost always what you want. .PARAMETER AsReport Add a ReportGeneratedUtc column. .EXAMPLE Get-GkLapsPassword -DeviceId $deviceId | Select-Object DeviceName, AccountName, Password Retrieve the current local administrator password for one device. .EXAMPLE Get-GkLapsPassword List the devices that have LAPS credentials, without reading any password. .EXAMPLE Get-GkDeviceInventory -StaleOnly | Get-GkLapsPassword -IncludePrevious .OUTPUTS PSGraphKit.LapsCredential #> [CmdletBinding()] [OutputType('PSGraphKit.LapsCredential')] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [string[]] $DeviceId, [switch] $IncludePrevious, [switch] $AsReport ) begin { # Listing needs only the ReadBasic scope; retrieving a password needs the full read. $scopeVariant = '' if ($PSBoundParameters.ContainsKey('DeviceId')) { $scopeVariant = 'Password' } Test-GkConnection -FunctionName 'Get-GkLapsPassword' -Variant $scopeVariant -Caller $PSCmdlet | Out-Null $now = [datetime]::UtcNow $listed = $false } process { if (-not $PSBoundParameters.ContainsKey('DeviceId')) { # Inventory mode: one call, and only on the first pipeline pass. if ($listed) { return } $listed = $true $infos = Invoke-GkGraphRequest -Uri '/directory/deviceLocalCredentials' -CallerFunction 'Get-GkLapsPassword' foreach ($i in $infos) { $obj = [ordered]@{ PSTypeName = 'PSGraphKit.LapsCredential' DeviceName = [string](Get-GkDictValue $i 'deviceName') DeviceId = [string](Get-GkDictValue $i 'id') AccountName = '' # the list endpoint returns no credential detail LastBackupDateTime = ConvertTo-GkDateTime (Get-GkDictValue $i 'lastBackupDateTime') RefreshDateTime = ConvertTo-GkDateTime (Get-GkDictValue $i 'refreshDateTime') IsCurrent = $true Password = $null # never returned by the list endpoint, by design } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } return } foreach ($did in $DeviceId) { if ([string]::IsNullOrWhiteSpace($did)) { continue } $enc = [uri]::EscapeDataString($did) try { $info = Invoke-GkGraphRequest -Raw -Uri "/directory/deviceLocalCredentials/$enc`?`$select=id,deviceName,lastBackupDateTime,refreshDateTime,credentials" ` -CallerFunction 'Get-GkLapsPassword' } catch { Write-Warning "Could not read LAPS credentials for device '$did': $($_.Exception.Message)" continue } $deviceName = [string](Get-GkDictValue $info 'deviceName') $lastBackup = ConvertTo-GkDateTime (Get-GkDictValue $info 'lastBackupDateTime') $refresh = ConvertTo-GkDateTime (Get-GkDictValue $info 'refreshDateTime') # credentials is newest-first; older entries exist only while the backup window holds them. $creds = @(Get-GkDictValue $info 'credentials') $index = 0 foreach ($c in $creds) { $isCurrent = ($index -eq 0) $index++ if (-not $isCurrent -and -not $IncludePrevious) { continue } # passwordBase64 is base64-encoded, not encrypted; decode it for the caller. $encoded = [string](Get-GkDictValue $c 'passwordBase64') $plain = $null if ($encoded) { try { $plain = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded)) } catch { $plain = $encoded } } $obj = [ordered]@{ PSTypeName = 'PSGraphKit.LapsCredential' DeviceName = $deviceName DeviceId = $did AccountName = [string](Get-GkDictValue $c 'accountName') LastBackupDateTime = ConvertTo-GkDateTime (Get-GkDictValue $c 'backupDateTime') RefreshDateTime = $refresh IsCurrent = $isCurrent Password = $plain } if (-not $obj['LastBackupDateTime']) { $obj['LastBackupDateTime'] = $lastBackup } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } } } } |