mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
268 lines
11 KiB
PowerShell
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
|