Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions backend/Modules/CIPPCore/Public/Get-CIPPMacOSLapsPassword.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function Get-CIPPMacOSLapsPassword {
<#
.SYNOPSIS
Retrieves the macOS LAPS local administrator password for an Intune managed device.
.DESCRIPTION
Reads the password Intune generated for the local admin account created at Apple automated
device enrollment (ADE). Only devices enrolled through an ADE profile that configures the local
admin account have one. Graph does not return the account name; it is set in the ADE profile.
.PARAMETER Device
The Intune managedDevice ID, not the Entra device ID used by Windows LAPS.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$Device,

[Parameter(Mandatory = $true)]
[string]$TenantFilter,

[Parameter(Mandatory = $false)]
[string]$APIName = 'Get macOS LAPS Password',

[Parameter(Mandatory = $false)]
[object]$Headers
)

# $Device comes straight off the request body and is interpolated into the Graph path, so a value
# with / or .. could otherwise address a different resource than the one device the caller named.
$ParsedDeviceId = [guid]::Empty
if (-not [guid]::TryParse($Device, [ref]$ParsedDeviceId)) {
throw [System.ArgumentException]::new("Invalid managed device ID for macOS LAPS password retrieval: $Device")
}

try {
$GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($ParsedDeviceId)/retrieveMacOSManagedDeviceLocalAdminAccountDetail" -tenantid $TenantFilter -ErrorAction Stop | Where-Object { $_.adminAccountPassword } | Select-Object -First 1 | ForEach-Object {
$RotatedDate = $_.passwordLastRotatedDateTime
[PSCustomObject]@{
resultText = "macOS LAPS password retrieved, last rotated at $RotatedDate. Copy the password by clicking the copy button"
copyField = $_.adminAccountPassword
passwordLastRotatedDateTime = $RotatedDate
state = 'success'
}
}
} catch {
$ErrorMessage = Get-CippException -Exception $_
Write-LogMessage -headers $Headers -API $APIName -message "Could not retrieve macOS LAPS password for $Device. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
throw "Could not retrieve macOS LAPS password for $Device. Error: $($ErrorMessage.NormalizedError)"
}
if (-not $GraphRequest) {
Write-LogMessage -headers $Headers -API $APIName -message "No macOS LAPS password found for $Device" -Sev 'Info' -tenant $TenantFilter
throw [System.Management.Automation.ItemNotFoundException]::new("No macOS LAPS password found for $Device")
}
Write-LogMessage -headers $Headers -API $APIName -message "Retrieved macOS LAPS password for $Device" -Sev 'Info' -tenant $TenantFilter
return $GraphRequest
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function Get-CIPPSchedulerBlockedCommands {

# End-tenant secret material - recovery keys & admin passwords, PostExecution would exfiltrate them
'Get-CIPPLAPSPassword'
'Get-CIPPMacOSLapsPassword'
'Get-CIPPBitlockerKey'
'Search-CIPPBitlockerKeys'
'Get-CIPPFileVaultKey'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ Function Invoke-ExecGetLocalAdminPassword {
.ROLE
Endpoint.Device.Read
.DESCRIPTION
Retrieves the Windows LAPS local administrator password for a device, identified by its Entra device GUID. Returns a live credential in plain text; the retrieval is written to the CIPP audit log.
Retrieves the LAPS local administrator password for a device. Platform selects Windows (the default), where guid is the Entra device ID, or macOS, where guid is the Intune managed device ID. Returns a live credential in plain text; the retrieval is written to the CIPP audit log.
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)

$APIName = $Request.Params.CIPPEndpoint

$Platform = $Request.Body.Platform ?? 'Windows'

if ($Platform -notin @('Windows', 'macOS')) {
return ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::BadRequest
Body = [pscustomobject]@{'Results' = "Invalid Platform specified: $Platform." }
})
}

try {
$GraphRequest = Get-CIPPLapsPassword -device $($request.body.guid) -tenantFilter $Request.body.TenantFilter -APIName $APINAME -Headers $Request.Headers
$GraphRequest = if ($Platform -eq 'macOS') {
Get-CIPPMacOSLapsPassword -Device $($request.body.guid) -TenantFilter $Request.body.TenantFilter -APIName $APINAME -Headers $Request.Headers
} else {
Get-CIPPLapsPassword -device $($request.body.guid) -tenantFilter $Request.body.TenantFilter -APIName $APINAME -Headers $Request.Headers
}
$Body = [pscustomobject]@{'Results' = $GraphRequest }
$StatusCode = [HttpStatusCode]::OK

Expand Down
81 changes: 81 additions & 0 deletions backend/Tests/Endpoint/Invoke-ExecGetLocalAdminPassword.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Pester tests for Invoke-ExecGetLocalAdminPassword.
#
# Thin endpoint over the two LAPS helpers. The part worth pinning is the Platform routing: existing
# callers send no Platform and must keep getting Windows LAPS, macOS must reach the helper keyed on the
# Intune managed device ID, and an unknown Platform must be refused rather than fall through to Windows.

BeforeAll {
$RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
$FunctionPath = Join-Path $RepoRoot 'Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetLocalAdminPassword.ps1'
if (-not (Test-Path $FunctionPath)) { throw "Could not locate Invoke-ExecGetLocalAdminPassword.ps1 at $FunctionPath" }

class HttpResponseContext {
[object]$StatusCode
[object]$Body
}
$Accelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
if (-not ('HttpStatusCode' -as [type])) {
$Accelerators::Add('HttpStatusCode', [System.Net.HttpStatusCode])
}

function Get-CIPPLapsPassword { param($Device, $TenantFilter, $APIName, $Headers) }
function Get-CIPPMacOSLapsPassword { param($Device, $TenantFilter, $APIName, $Headers) }
. (Join-Path $RepoRoot 'Modules/CIPPCore/Public/Get-CippErrorStatusCode.ps1')

. $FunctionPath

function New-LapsRequest {
param($Platform)
$Body = [pscustomobject]@{ TenantFilter = 'contoso.com'; guid = 'device-guid' }
if ($null -ne $Platform) { $Body | Add-Member -NotePropertyName Platform -NotePropertyValue $Platform }
[pscustomobject]@{
Body = $Body
Query = [pscustomobject]@{}
Params = [pscustomobject]@{ CIPPEndpoint = 'ExecGetLocalAdminPassword' }
Headers = @{}
}
}
}

Describe 'Invoke-ExecGetLocalAdminPassword' {
BeforeEach {
Mock -CommandName Get-CIPPLapsPassword -MockWith { 'windows' }
Mock -CommandName Get-CIPPMacOSLapsPassword -MockWith { 'macos' }
}

It 'retrieves Windows LAPS when no Platform is sent' {
$Response = Invoke-ExecGetLocalAdminPassword -Request (New-LapsRequest) -TriggerMetadata $null

$Response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK)
$Response.Body.Results | Should -Be 'windows'
Should -Invoke Get-CIPPLapsPassword -Times 1 -ParameterFilter { $Device -eq 'device-guid' -and $TenantFilter -eq 'contoso.com' }
Should -Invoke Get-CIPPMacOSLapsPassword -Times 0
}

It 'retrieves macOS LAPS when Platform is macOS' {
$Response = Invoke-ExecGetLocalAdminPassword -Request (New-LapsRequest -Platform 'macOS') -TriggerMetadata $null

$Response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK)
$Response.Body.Results | Should -Be 'macos'
Should -Invoke Get-CIPPMacOSLapsPassword -Times 1 -ParameterFilter { $Device -eq 'device-guid' -and $TenantFilter -eq 'contoso.com' }
Should -Invoke Get-CIPPLapsPassword -Times 0
}

It 'returns 400 for an unknown Platform without calling either helper' {
$Response = Invoke-ExecGetLocalAdminPassword -Request (New-LapsRequest -Platform 'Linux') -TriggerMetadata $null

$Response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::BadRequest)
$Response.Body.Results | Should -Be 'Invalid Platform specified: Linux.'
Should -Invoke Get-CIPPLapsPassword -Times 0
Should -Invoke Get-CIPPMacOSLapsPassword -Times 0
}

It 'returns 404 when the device has no macOS LAPS password' {
Mock -CommandName Get-CIPPMacOSLapsPassword -MockWith { throw [System.Management.Automation.ItemNotFoundException]::new('No macOS LAPS password found for device-guid') }

$Response = Invoke-ExecGetLocalAdminPassword -Request (New-LapsRequest -Platform 'macOS') -TriggerMetadata $null

$Response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::NotFound)
$Response.Body.Results | Should -Be 'No macOS LAPS password found for device-guid'
}
}
46 changes: 46 additions & 0 deletions backend/Tests/Private/Get-CIPPMacOSLapsPassword.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
BeforeAll {
$RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
. (Join-Path $RepoRoot 'Modules/CIPPCore/Public/Get-CIPPMacOSLapsPassword.ps1')

function New-GraphGetRequest { param($uri, $tenantid) }
function Write-LogMessage { param($headers, $API, $message, $Sev, $tenant, $LogData) }
function Get-CippException { param($Exception) [pscustomobject]@{ NormalizedError = [string]$Exception.Exception.Message } }

$script:DeviceId = '11111111-2222-3333-4444-555555555555'
}

Describe 'Get-CIPPMacOSLapsPassword' {
It 'returns the credential when one exists' {
Mock New-GraphGetRequest { [pscustomobject]@{ adminAccountPassword = 'secret'; passwordLastRotatedDateTime = '2026-09-01T00:00:00Z' } }
$Result = Get-CIPPMacOSLapsPassword -Device $script:DeviceId -TenantFilter 'contoso.com'
$Result.state | Should -Be 'success'
$Result.copyField | Should -Be 'secret'
$Result.passwordLastRotatedDateTime | Should -Be '2026-09-01T00:00:00Z'
}

It 'calls the managed device function for the one device named' {
Mock New-GraphGetRequest { [pscustomobject]@{ adminAccountPassword = 'secret' } }
Get-CIPPMacOSLapsPassword -Device $script:DeviceId -TenantFilter 'contoso.com'
Should -Invoke New-GraphGetRequest -Times 1 -ParameterFilter {
$uri -eq "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($script:DeviceId)/retrieveMacOSManagedDeviceLocalAdminAccountDetail" -and $tenantid -eq 'contoso.com'
}
}

It 'throws ArgumentException without calling Graph when the device ID is not a GUID' {
Mock New-GraphGetRequest { }
{ Get-CIPPMacOSLapsPassword -Device '../../users' -TenantFilter 'contoso.com' } | Should -Throw -ExceptionType ([System.ArgumentException])
Should -Invoke New-GraphGetRequest -Times 0
}

It 'throws ItemNotFoundException when the device has no LAPS password' {
Mock New-GraphGetRequest { [pscustomobject]@{ adminAccountPassword = $null; passwordLastRotatedDateTime = $null } }
{ Get-CIPPMacOSLapsPassword -Device $script:DeviceId -TenantFilter 'contoso.com' } | Should -Throw "No macOS LAPS password found for $($script:DeviceId)" -ExceptionType ([System.Management.Automation.ItemNotFoundException])
}

It 'throws an untyped error when Graph fails' {
Mock New-GraphGetRequest { throw 'Forbidden' }
$Err = { Get-CIPPMacOSLapsPassword -Device $script:DeviceId -TenantFilter 'contoso.com' } | Should -Throw "Could not retrieve macOS LAPS password for $($script:DeviceId). Error: Forbidden" -PassThru
$Err.Exception | Should -Not -BeOfType ([System.Management.Automation.ItemNotFoundException])
$Err.Exception | Should -Not -BeOfType ([System.ArgumentException])
}
}
Loading
Loading