From a78c29744ea46886e81f4ebc8db8230dca9c35c1 Mon Sep 17 00:00:00 2001 From: Marco Wende Date: Wed, 16 Sep 2026 11:21:24 +0200 Subject: [PATCH] =?UTF-8?q?Offboarding:=20Autopilot-Ger=C3=A4te=20mitdurch?= =?UTF-8?q?suchen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Offboarding-Suche fand bisher nur managedDevices (Intune). Reine Autopilot-Geräte (registriert, aber nicht/nicht mehr in Intune enrolled) tauchten nie auf. Jetzt wird zusätzlich windowsAutopilotDeviceIdentities durchsucht (Serial: contains(serialNumber), Name: startswith(displayName), Fehler still ignoriert) und die Treffer werden — dedupliziert per Serial/ Autopilot-Id — gemergt. Für Autopilot-only-Treffer wird das Entra-Objekt per azureAdDeviceId nachgeladen. offKey nutzt jetzt eine Fallback-Kette, da Autopilot-only-Geräte keine intuneDeviceId haben. Co-Authored-By: Claude Opus 4.8 --- src/Offboard.ps1 | 89 ++++++++++++++++++++++++++++++++++++++++++++++-- www/app.js | 4 ++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/Offboard.ps1 b/src/Offboard.ps1 index c65156d..aa3a583 100644 --- a/src/Offboard.ps1 +++ b/src/Offboard.ps1 @@ -54,8 +54,93 @@ function Search-OffboardDevicesEndpoint { } return @{ __status = 500; error = "Graph-Fehler bei der Suche: $m" } } - if ($intune.Count -eq 0) { return @{ items = @(); count = 0 } } - return @{ items = @(Resolve-OffboardItems -IntuneDevices $intune); count = $intune.Count } + $items = @() + if ($intune.Count -gt 0) { $items = @(Resolve-OffboardItems -IntuneDevices $intune) } + + # Zusaetzlich Autopilot direkt durchsuchen: Geraete, die in Autopilot registriert, + # aber (noch) nicht in Intune enrolled sind (oder aus Intune entfernt wurden), + # tauchen in der managedDevices-Suche nicht auf. + $apItems = @(Search-AutopilotOnly -Query $q -Type $type -ExistingItems $items) + $items = @($items) + @($apItems) + + return @{ items = @($items); count = @($items).Count } +} + +# Durchsucht windowsAutopilotDeviceIdentities direkt und liefert Offboard-Items fuer +# Geraete, die noch nicht ueber die Intune-Suche abgedeckt sind. Namenssuche laeuft +# ueber displayName (nicht garantiert unterstuetzt -> Fehler werden still ignoriert), +# Seriennummer ueber contains(serialNumber). +function Search-AutopilotOnly { + param([string]$Query, [string]$Type, [object[]]$ExistingItems) + $qEsc = $Query -replace "'", "''" + $apFilter = if ($Type -eq 'serial') { "contains(serialNumber,'$qEsc')" } else { "startswith(displayName,'$qEsc')" } + $sel = 'id,serialNumber,displayName,azureAdDeviceId,managedDeviceId,userPrincipalName,model,manufacturer,groupTag,enrollmentState' + $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$filter=$([Uri]::EscapeDataString($apFilter))&`$select=$sel&`$top=50" + $ap = @() + try { + $ap = @(Get-GraphPaged -Uri $uri) + } catch { + $m = Get-OffboardGraphErr $_ + Write-Host " [OFFBOARD] Autopilot-Suche ($Type='$Query'): $m" -ForegroundColor DarkYellow + return @() + } + if ($ap.Count -eq 0) { return @() } + + # Bereits durch die Intune-Suche abgedeckte Geraete rausfiltern (Seriennummer/Autopilot-Id). + $seenSer = @{}; $seenApId = @{} + foreach ($it in @($ExistingItems)) { + $s = ([string]$it.serialNumber).Trim().ToLower(); if ($s) { $seenSer[$s] = $true } + $aid = [string]$it.autopilotId; if ($aid) { $seenApId[$aid] = $true } + } + $new = @($ap | Where-Object { + $s = ([string]$_.serialNumber).Trim().ToLower() + $aid = [string]$_.id + -not ($seenApId[$aid] -or ($s -and $seenSer[$s])) + }) + if ($new.Count -eq 0) { return @() } + + # Entra-Objekt per azureAdDeviceId nachladen (fuer Delete). + $reqs = @(); $idx = 0 + foreach ($a in $new) { + $aad = [string]$a.azureAdDeviceId + if ($aad -and $aad -ne '00000000-0000-0000-0000-000000000000') { + $f = [Uri]::EscapeDataString("deviceId eq '$aad'") + $reqs += @{ id = "e$idx"; method = 'GET'; url = "/devices?`$filter=$f&`$select=id,deviceId,displayName,accountEnabled&`$top=1" } + } + $idx++ + } + $batch = if ($reqs.Count -gt 0) { Invoke-GraphBatch -Requests $reqs } else { @{} } + + $items = @(); $idx = 0 + foreach ($a in $new) { + $entra = $null + $er = $batch["e$idx"]; if ($er -and [int]$er.status -eq 200) { $entra = @($er.body.value)[0] } + $mdid = [string]$a.managedDeviceId + $hasIntune = ($mdid -and $mdid -ne '00000000-0000-0000-0000-000000000000') + $dn = [string]$a.displayName + if (-not $dn) { $dn = if ($a.serialNumber) { "SN $([string]$a.serialNumber)" } else { '(Autopilot-Geraet)' } } + $items += [pscustomobject]@{ + deviceName = $dn + serialNumber = [string]$a.serialNumber + operatingSystem = 'Windows' + osVersion = '' + primaryUser = [string]$a.userPrincipalName + lastSync = $null + ownership = '' + coManaged = $false + intuneDeviceId = if ($hasIntune) { $mdid } else { '' } + azureADDeviceId = [string]$a.azureAdDeviceId + entraObjectId = if ($entra) { [string]$entra.id } else { '' } + entraEnabled = if ($entra) { [bool]$entra.accountEnabled } else { $null } + autopilotId = [string]$a.id + autopilotNote = '' + inIntune = [bool]$hasIntune + inEntra = [bool]$entra + inAutopilot = $true + } + $idx++ + } + return @($items) } # Reichert Intune-managedDevice-Objekte mit Entra-Objekt-Id + Autopilot-Id an diff --git a/www/app.js b/www/app.js index df80017..5fa54c4 100644 --- a/www/app.js +++ b/www/app.js @@ -7077,7 +7077,9 @@ document.querySelectorAll('#polTable th.sortable').forEach(th => { // OFFBOARDING: Geräte aus Intune / Autopilot / Entra entfernen // ============================================================= const OffState = { items: [], selected: new Set(), pending: [] }; -function offKey(d) { return d.intuneDeviceId; } +// Eindeutiger Zeilen-Key: Intune-Geräte haben eine intuneDeviceId, reine +// Autopilot-Geräte nicht -> auf Autopilot-/Entra-Id bzw. Seriennummer ausweichen. +function offKey(d) { return d.intuneDeviceId || d.autopilotId || d.azureADDeviceId || d.entraObjectId || d.serialNumber || ''; } function offSelectedDevices() { return OffState.items.filter(d => OffState.selected.has(offKey(d))); } async function offSearch() {