#Requires -Version 7.0 <# .SYNOPSIS Intune Per-Device Policy Drift Investigation move2modern.co.uk — Part 1 companion script .DESCRIPTION Run this after Get-IntuneComplianceDrift.ps1 has identified drifted devices. Takes a Device ID (from the drift report CSV) and shows exactly which policies are causing the problem on that specific device — including conflict counts, which are the most common and painful real-world cause. .HOW TO USE Option A — use the DeviceId from the DriftReport CSV (most reliable): .\Get-DevicePolicyDrift.ps1 -DeviceId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" Option B — use the device name as shown in Intune: .\Get-DevicePolicyDrift.ps1 -DeviceName "DESKTOP-ABC123" Option C — run the script with no parameters and it will prompt you. Paste either the DeviceId or DeviceName when asked. The script will detect which you've entered automatically. #> param( [string]$DeviceId, [string]$DeviceName ) # ───────────────────────────────────────────────────────────────────────────── # HELPERS # ───────────────────────────────────────────────────────────────────────────── function Write-Header ($Text) { Write-Host "" Write-Host " $Text" -ForegroundColor Cyan Write-Host (" " + ("─" * ($Text.Length))) -ForegroundColor DarkGray } function Write-Ok ($Msg) { Write-Host " [OK] $Msg" -ForegroundColor Green } function Write-Warn ($Msg) { Write-Host " [!!] $Msg" -ForegroundColor Yellow } function Write-Info ($Msg) { Write-Host " $Msg" -ForegroundColor Gray } # ───────────────────────────────────────────────────────────────────────────── # BANNER # ───────────────────────────────────────────────────────────────────────────── Write-Host "" Write-Host " ╔═══════════════════════════════════════════════════════╗" -ForegroundColor Cyan Write-Host " ║ Intune Per-Device Policy Drift Investigation ║" -ForegroundColor Cyan Write-Host " ║ move2modern.co.uk ║" -ForegroundColor Cyan Write-Host " ╚═══════════════════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" Write-Info "Run Get-IntuneComplianceDrift.ps1 first to identify drifted devices." Write-Info "Then use the Device ID from that report's CSV to investigate here." # ───────────────────────────────────────────────────────────────────────────── # STEP 1 — MODULE CHECK # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 1 — Checking modules" foreach ($Mod in @('Microsoft.Graph.Authentication', 'Microsoft.Graph.DeviceManagement')) { if (Get-Module -ListAvailable -Name $Mod) { Write-Ok "$Mod is installed" } else { Write-Warn "$Mod not found — installing..." try { Install-Module $Mod -Scope CurrentUser -Force -ErrorAction Stop Write-Ok "$Mod installed successfully" } catch { Write-Host " [XX] Could not install $Mod : $_" -ForegroundColor Red Read-Host " Press Enter to close" exit 1 } } } # ───────────────────────────────────────────────────────────────────────────── # STEP 2 — CONNECT # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 2 — Connecting to Microsoft Graph" try { Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.Read.All' ` -NoWelcome -ErrorAction Stop $ctx = Get-MgContext Write-Ok "Connected as : $($ctx.Account)" Write-Info "Tenant : $($ctx.TenantId)" } catch { Write-Host " [XX] Connection failed: $_" -ForegroundColor Red Read-Host " Press Enter to close" exit 1 } # ───────────────────────────────────────────────────────────────────────────── # STEP 3 — IDENTIFY DEVICE # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 3 — Device to investigate" Write-Host "" Write-Info "You can provide either the DeviceId (from the DriftReport CSV)" Write-Info "or the DeviceName (exactly as it appears in Intune)." Write-Host "" # Prompt if neither was passed as a parameter if (-not $DeviceId -and -not $DeviceName) { $Input = Read-Host " Device Name or Device ID" $Input = $Input.Trim() # Heuristic: GUIDs are 36 chars with hyphens — treat everything else as a name if ($Input -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}$') { $DeviceId = $Input } else { $DeviceName = $Input } } # If we have a name but no ID, look it up via Graph API directly # (using Invoke-MgGraphRequest avoids the empty-property issue with SDK filters) if ($DeviceName -and -not $DeviceId) { Write-Info "Looking up '$DeviceName'..." try { $EncodedName = [Uri]::EscapeDataString($DeviceName) $Response = Invoke-MgGraphRequest ` -Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?`$filter=deviceName eq '$EncodedName'&`$select=id,deviceName,userPrincipalName,complianceState,operatingSystem" ` -Method GET -ErrorAction Stop $Lookup = $Response.value if ($Lookup.Count -eq 0) { Write-Host " [XX] No device found with name '$DeviceName'." -ForegroundColor Red Write-Info "Check the name matches exactly as shown in Intune (case-sensitive)." Write-Info "Alternatively, use the DeviceId from the DriftReport CSV." Disconnect-MgGraph Read-Host " Press Enter to close" exit 1 } if ($Lookup.Count -gt 1) { Write-Warn "Multiple devices found with that name — picking the first match." Write-Info "If this is wrong, re-run using -DeviceId from the CSV instead." } $Found = $Lookup[0] $DeviceId = $Found.id $Device = [PSCustomObject]@{ DeviceName = $Found.deviceName UserPrincipalName = $Found.userPrincipalName ComplianceState = $Found.complianceState OperatingSystem = $Found.operatingSystem } Write-Ok "Device resolved: $($Device.DeviceName) → $DeviceId" } catch { Write-Host " [XX] Name lookup failed: $_" -ForegroundColor Red Disconnect-MgGraph Read-Host " Press Enter to close" exit 1 } } else { # We have an ID — fetch device details via Graph API directly $DeviceId = $DeviceId.Trim() try { $Found = Invoke-MgGraphRequest ` -Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId`?`$select=id,deviceName,userPrincipalName,complianceState,operatingSystem" ` -Method GET -ErrorAction Stop $Device = [PSCustomObject]@{ DeviceName = $Found.deviceName UserPrincipalName = $Found.userPrincipalName ComplianceState = $Found.complianceState OperatingSystem = $Found.operatingSystem } } catch { Write-Host " [XX] Device ID not found. Check the ID is correct and belongs to this tenant." -ForegroundColor Red Disconnect-MgGraph Read-Host " Press Enter to close" exit 1 } } Write-Host "" Write-Ok "Device found : $($Device.DeviceName)" Write-Info "User : $($Device.UserPrincipalName)" Write-Info "OS : $($Device.OperatingSystem)" Write-Info "Overall state : $($Device.ComplianceState)" # ───────────────────────────────────────────────────────────────────────────── # STEP 4 — RETRIEVE POLICY STATES # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 4 — Retrieving policy states" try { # Use Invoke-MgGraphRequest directly — the SDK cmdlet for this endpoint # is not available in all Microsoft.Graph module configurations $Response = Invoke-MgGraphRequest ` -Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId/deviceConfigurationStates" ` -Method GET -ErrorAction Stop $AllStates = $Response.value Write-Ok "Total policies assigned to this device: $($AllStates.Count)" } catch { $ErrDetail = $_.ErrorDetails.Message if (-not $ErrDetail) { $ErrDetail = $_.Exception.Message } Write-Host " [XX] Failed to retrieve policy states: $ErrDetail" -ForegroundColor Red Disconnect-MgGraph Read-Host " Press Enter to close" exit 1 } # ───────────────────────────────────────────────────────────────────────────── # STEP 5 — DISPLAY RESULTS # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 5 — Policy drift results for $($Device.DeviceName)" Write-Host "" $ProblemStates = $AllStates | Where-Object { $_.state -notin @('compliant', 'notApplicable') } if ($ProblemStates.Count -eq 0) { Write-Host " ✅ No policy-level drift found on this device." -ForegroundColor Green Write-Info "The device may show as non-compliant due to a compliance policy," Write-Info "not a configuration profile. Check the compliance policy status in Intune." } else { Write-Host (" ⚠ {0} policy/policies with issues found ({1} total assigned)" -f ` $ProblemStates.Count, $AllStates.Count) -ForegroundColor Yellow Write-Host "" # ── Conflicts first — these are the important ones ──────────────────── $Conflicts = $ProblemStates | Where-Object { $_.conflictCount -gt 0 } if ($Conflicts.Count -gt 0) { Write-Host " CONFLICTS (two or more policies fighting over the same setting)" ` -ForegroundColor Red Write-Host " ─────────────────────────────────────────────────────────────────" ` -ForegroundColor DarkGray foreach ($Item in $Conflicts | Sort-Object conflictCount -Descending) { Write-Host (" ❌ {0}" -f $Item.displayName) -ForegroundColor Red Write-Host (" State: {0} Conflicts: {1} Errors: {2}" -f ` $Item.state, $Item.conflictCount, $Item.errorCount) -ForegroundColor DarkRed } Write-Host "" Write-Warn "Conflicts are not fixed by a device sync — they need a policy review." Write-Info "Go to Intune > Devices > [this device] > Configuration to see which" Write-Info "policies overlap and which setting is being contested." Write-Host "" } # ── Errors ──────────────────────────────────────────────────────────── $Errors = $ProblemStates | Where-Object { $_.state -eq 'error' -and $_.conflictCount -eq 0 } if ($Errors.Count -gt 0) { Write-Host " ERRORS (policy failed to apply)" -ForegroundColor Red Write-Host " ─────────────────────────────────────────────────────────────────" ` -ForegroundColor DarkGray foreach ($Item in $Errors) { Write-Host (" ❌ {0}" -f $Item.displayName) -ForegroundColor Red Write-Host (" State: {0} Errors: {1}" -f $Item.state, $Item.errorCount) ` -ForegroundColor DarkRed } Write-Host "" } # ── Non-compliant (no conflict, no error) ──────────────────────────── $NonCompliant = $ProblemStates | Where-Object { $_.state -eq 'nonCompliant' -and $_.conflictCount -eq 0 } if ($NonCompliant.Count -gt 0) { Write-Host " NON-COMPLIANT (settings don't match policy requirements)" ` -ForegroundColor Yellow Write-Host " ─────────────────────────────────────────────────────────────────" ` -ForegroundColor DarkGray foreach ($Item in $NonCompliant) { Write-Host (" ⚠ {0}" -f $Item.displayName) -ForegroundColor Yellow Write-Host (" State: {0} Settings checked: {1}" -f ` $Item.state, $Item.settingCount) -ForegroundColor DarkYellow } Write-Host "" } # ── Everything else (unknown, inGracePeriod, etc.) ─────────────────── $Other = $ProblemStates | Where-Object { $_.state -notin @('nonCompliant', 'error') -and $_.conflictCount -eq 0 } if ($Other.Count -gt 0) { Write-Host " OTHER STATES" -ForegroundColor Gray Write-Host " ─────────────────────────────────────────────────────────────────" ` -ForegroundColor DarkGray foreach ($Item in $Other) { Write-Host (" • {0} [{1}]" -f $Item.displayName, $Item.state) -ForegroundColor Gray } Write-Host "" } } # ───────────────────────────────────────────────────────────────────────────── # STEP 6 — SAVE RESULTS # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 6 — Saving results" $OutputFolder = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'DriftBaselines' $OutputFile = Join-Path $OutputFolder "PolicyDrift_$($Device.DeviceName -replace '[^a-zA-Z0-9]','_')_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" if (-not (Test-Path $OutputFolder)) { New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null } if ($ProblemStates.Count -gt 0) { $ProblemStates | Select-Object ` @{ N = 'PolicyName'; E = { $_.displayName } }, @{ N = 'State'; E = { $_.state } }, @{ N = 'ConflictCount'; E = { $_.conflictCount } }, @{ N = 'ErrorCount'; E = { $_.errorCount } }, @{ N = 'SettingCount'; E = { $_.settingCount } } | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8 Write-Ok "Results saved to: $OutputFile" } else { Write-Info "Nothing to save — no policy drift detected." } # ───────────────────────────────────────────────────────────────────────────── # DONE # ───────────────────────────────────────────────────────────────────────────── Write-Host "" Write-Host " ─────────────────────────────────────────────────────────" -ForegroundColor DarkGray Write-Host " Done · move2modern.co.uk" -ForegroundColor Cyan Write-Host " ─────────────────────────────────────────────────────────" -ForegroundColor DarkGray Write-Host "" Disconnect-MgGraph Read-Host " Press Enter to close"