fleet/docs/solutions/windows/scripts/fix-windows-mdm-migration.ps1

268 lines
11 KiB
PowerShell

# Please don't delete. This script is referenced in the guide here:
# https://fleetdm.com/guides/windows-mdm-setup#migrating-from-another-mdm-solution
#
# fix-windows-mdm-migration.ps1
# Comprehensive remediation script for Windows hosts migrated to Fleet from another MDM solution
# (e.g., Microsoft Intune). Each fix is gated by a detection check so it only runs if needed.
#
# This script addresses:
# 1. Incorrect MDM enrollment flag
# 2. Stale/orphaned MDM enrollment records and caches
# 3. Broken Workplace Join configuration
# 4. Unreachable WSUS server configuration
# 5. Stale EnterpriseMgmt scheduled tasks
# 6. Local account lockout caused by tattooed LocalUsersAndGroups policies
#
# Usage: Run via Fleet on affected hosts. Reboot the device after running this script.
# Reference: https://github.com/fleetdm/fleet/issues/38985
#Requires -RunAsAdministrator
# Log output for audit trail
$logPath = "$env:TEMP\fleet-mdm-migration-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
Start-Transcript -Path $logPath -ErrorAction SilentlyContinue | Out-Null
$fixesApplied = 0
Write-Host "=== Fleet Windows MDM Migration Remediation ==="
Write-Host "Log file: $logPath"
Write-Host ""
# ---------------------------------------------------------------------------
# 1. Reset MDM enrollment flag
# ---------------------------------------------------------------------------
Write-Host "[1/6] Checking MDM enrollment flag..."
$enrollmentsPath = "HKLM:\SOFTWARE\Microsoft\Enrollments"
$enrollmentFlag = (Get-ItemProperty -Path $enrollmentsPath -Name "MmpcEnrollmentFlag" -ErrorAction SilentlyContinue).MmpcEnrollmentFlag
if ($null -ne $enrollmentFlag -and 0 -ne $enrollmentFlag) {
Write-Host " Enrollment flag is $enrollmentFlag - resetting to 0"
Set-ItemProperty -Path $enrollmentsPath -Name "MmpcEnrollmentFlag" -Value 0 -Type DWord
$fixesApplied++
} else {
Write-Host " Enrollment flag already 0 or does not exist - skipping"
}
Write-Host ""
# ---------------------------------------------------------------------------
# 2. Remove stale MDM enrollment records, AAD cache, and MS DM Server cache
# ---------------------------------------------------------------------------
Write-Host "[2/6] Cleaning stale enrollment records and caches..."
$cachesCleaned = $false
# Clear the AAD discovery cache
$AADPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CDJ\AAD"
if (Test-Path $AADPath) {
Remove-Item -Path $AADPath -Recurse -Force
Write-Host " Cleared AAD discovery cache"
$cachesCleaned = $true
$fixesApplied++
} else {
Write-Host " AAD discovery cache not found - skipping"
}
# Remove GUID-based enrollment entries in failed, removed, or error states
# EnrollmentState: 0=Not enrolled, 1=Enrolled, 2=Failed, 3=Removed, 4=Error
# We preserve state 0 (could be a pending Fleet enrollment) and state 1 (active)
$EnrollmentPath = "HKLM:\SOFTWARE\Microsoft\Enrollments"
$cleaned = 0
Get-ChildItem -Path $EnrollmentPath -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.PSChildName -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') {
$state = (Get-ItemProperty -Path $_.PSPath -Name "EnrollmentState" -ErrorAction SilentlyContinue).EnrollmentState
# Remove failed (2), removed (3), error (4) states and orphaned entries with no state
if ($state -in @(2, 3, 4) -or $null -eq $state) {
Write-Host " Removing enrollment: $($_.PSChildName) (state: $state)"
Remove-Item -Path $_.PSPath -Recurse -Force -ErrorAction SilentlyContinue
$cleaned++
}
}
}
if ($cleaned -gt 0) {
Write-Host " Cleaned $cleaned stale enrollment entries"
$cachesCleaned = $true
$fixesApplied++
} else {
Write-Host " No stale enrollment entries found"
}
# Clear MS DM Server cache
if (Test-Path "HKLM:\SOFTWARE\Microsoft\MSDM\Server") {
Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\MSDM\Server\*" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " Cleared MS DM Server cache"
$cachesCleaned = $true
$fixesApplied++
} else {
Write-Host " MS DM Server cache not found - skipping"
}
# Restart Device Registration Service only if we made changes
if ($cachesCleaned) {
Restart-Service -Name "DsSvc" -ErrorAction SilentlyContinue
Write-Host " Restarted Device Registration Service"
}
Write-Host ""
# ---------------------------------------------------------------------------
# 3. Fix Workplace Join configuration
# ---------------------------------------------------------------------------
Write-Host "[3/6] Checking Workplace Join configuration..."
# Re-enable Automatic-Device-Join scheduled task
$TaskPath = "\Microsoft\Windows\Workplace Join\"
$TaskName = "Automatic-Device-Join"
try {
$task = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction Stop
if ($task.State -eq "Disabled") {
Enable-ScheduledTask -InputObject $task | Out-Null
Write-Host " Re-enabled Automatic-Device-Join task"
$fixesApplied++
} else {
Write-Host " Automatic-Device-Join task already enabled"
}
} catch {
Write-Host " Automatic-Device-Join task not found - skipping"
}
# Configure Workplace Join policy
$WJPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WorkplaceJoin"
$needsUpdate = $false
if (-not (Test-Path $WJPath)) {
$needsUpdate = $true
} else {
$autoJoin = (Get-ItemProperty -Path $WJPath -Name "autoWorkplaceJoin" -ErrorAction SilentlyContinue).autoWorkplaceJoin
$blockJoin = (Get-ItemProperty -Path $WJPath -Name "BlockAADWorkplaceJoin" -ErrorAction SilentlyContinue).BlockAADWorkplaceJoin
if ($autoJoin -ne 1 -or $blockJoin -ne 0) { $needsUpdate = $true }
}
if ($needsUpdate) {
if (-not (Test-Path $WJPath)) { New-Item -Path $WJPath -Force | Out-Null }
Set-ItemProperty -Path $WJPath -Name "autoWorkplaceJoin" -Value 1 -Type DWord
Set-ItemProperty -Path $WJPath -Name "BlockAADWorkplaceJoin" -Value 0 -Type DWord
Write-Host " Configured Workplace Join policy (autoJoin=1, BlockAAD=0)"
$fixesApplied++
} else {
Write-Host " Workplace Join policy already configured correctly"
}
Write-Host ""
# ---------------------------------------------------------------------------
# 4. Remove unreachable WSUS configuration
# ---------------------------------------------------------------------------
Write-Host "[4/6] Checking WSUS configuration..."
$WUPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
$wuServer = $null
if (Test-Path $WUPath) {
$wuServer = (Get-ItemProperty -Path $WUPath -Name "WUServer" -ErrorAction SilentlyContinue).WUServer
}
if ($wuServer) {
$reachable = $false
try {
$null = Invoke-WebRequest -Uri $wuServer -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
$reachable = $true
} catch [System.Net.WebException] {
# HTTP error responses (e.g. 403) mean the server is reachable
if ($_.Exception.Response) { $reachable = $true }
} catch { }
if ($reachable) {
Write-Host " WSUS server $wuServer is reachable - no action taken"
} else {
Write-Host " WSUS server $wuServer is unreachable - removing configuration"
Remove-ItemProperty -Path $WUPath -Name "WUServer" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path $WUPath -Name "WUStatusServer" -ErrorAction SilentlyContinue
Restart-Service wuauserv -Force -ErrorAction SilentlyContinue
Write-Host " Removed WSUS config and restarted Windows Update service"
$fixesApplied++
}
} else {
Write-Host " No WSUS server configured - skipping"
}
Write-Host ""
# ---------------------------------------------------------------------------
# 5. Unregister stale EnterpriseMgmt scheduled tasks
# ---------------------------------------------------------------------------
Write-Host "[5/6] Checking EnterpriseMgmt scheduled tasks..."
$emTasks = Get-ScheduledTask -TaskPath "\Microsoft\Windows\EnterpriseMgmt\*" -ErrorAction SilentlyContinue
if ($emTasks) {
$emTasks | Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " Unregistered $($emTasks.Count) stale EnterpriseMgmt tasks"
$fixesApplied++
} else {
Write-Host " No EnterpriseMgmt tasks found - skipping"
}
Write-Host ""
# ---------------------------------------------------------------------------
# 6. Fix local account lockout from tattooed LocalUsersAndGroups policies
# ---------------------------------------------------------------------------
Write-Host "[6/6] Checking for tattooed LocalUsersAndGroups policies..."
$lockoutFixed = $false
# Check for orphaned LocalUsersAndGroups in PolicyManager current device path
$lugCurrentPath = "HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device\LocalUsersAndGroups"
if (Test-Path $lugCurrentPath) {
Remove-Item -Path $lugCurrentPath -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " Removed orphaned LocalUsersAndGroups from PolicyManager current device"
$lockoutFixed = $true
}
# Check for orphaned LocalUsersAndGroups in PolicyManager provider paths
$providersPath = "HKLM:\SOFTWARE\Microsoft\PolicyManager\Providers"
if (Test-Path $providersPath) {
Get-ChildItem -Path $providersPath -ErrorAction SilentlyContinue | ForEach-Object {
$lugProviderPath = Join-Path $_.PSPath "default\Device\LocalUsersAndGroups"
if (Test-Path $lugProviderPath) {
Remove-Item -Path $lugProviderPath -Recurse -Force -ErrorAction SilentlyContinue
Write-Host " Removed orphaned LocalUsersAndGroups from provider: $($_.PSChildName)"
$lockoutFixed = $true
}
}
}
if ($lockoutFixed) {
# Reset SeInteractiveLogonRight to Windows defaults
# Default: Administrators (S-1-5-32-544), Users (S-1-5-32-545), Backup Operators (S-1-5-32-551)
$tempCfg = "$env:TEMP\secpol_fix.cfg"
$tempDb = "$env:TEMP\secpol_fix.sdb"
try {
# Export current security policy
secedit /export /cfg $tempCfg /quiet 2>$null
# Read, patch, and write the config
$content = Get-Content $tempCfg -Raw
$defaultRight = "*S-1-5-32-544,*S-1-5-32-545,*S-1-5-32-551"
if ($content -match "SeInteractiveLogonRight") {
$content = $content -replace "SeInteractiveLogonRight\s*=.*", "SeInteractiveLogonRight = $defaultRight"
} else {
$content = $content -replace "(\[Privilege Rights\])", "$1`r`nSeInteractiveLogonRight = $defaultRight"
}
Set-Content -Path $tempCfg -Value $content
# Apply the patched config
secedit /configure /db $tempDb /cfg $tempCfg /quiet 2>$null
Write-Host " Reset SeInteractiveLogonRight to defaults (Administrators, Users, Backup Operators)"
$fixesApplied++
} catch {
Write-Host " Warning: Failed to reset SeInteractiveLogonRight - $_"
} finally {
Remove-Item $tempCfg -Force -ErrorAction SilentlyContinue
Remove-Item $tempDb -Force -ErrorAction SilentlyContinue
}
} else {
Write-Host " No orphaned LocalUsersAndGroups policies found - skipping"
}
Write-Host ""
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
Write-Host "=== Remediation Complete ==="
Write-Host "Fixes applied: $fixesApplied"
if ($fixesApplied -gt 0) {
Write-Host ""
Write-Host "IMPORTANT: Reboot the device now to apply changes."
Write-Host "After reboot, select Refetch on the host details page in Fleet."
} else {
Write-Host "No issues detected - device appears healthy."
}
Write-Host "Log saved to: $logPath"
Stop-Transcript -ErrorAction SilentlyContinue | Out-Null