#Requires -Version 7.0 <# .SYNOPSIS Intune Compliance Drift Detection move2modern.co.uk — Part 1 companion script .DESCRIPTION Connects interactively to Microsoft Graph, retrieves all managed devices, flags those with non-compliant or unknown compliance states, displays results in the console, and saves a CSV to Documents\DriftBaselines. .NOTES Required permissions (granted at sign-in prompt): DeviceManagementManagedDevices.Read.All #> # ───────────────────────────────────────────────────────────────────────────── # SETUP # ───────────────────────────────────────────────────────────────────────────── $OutputFolder = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'DriftBaselines' $OutputFile = Join-Path $OutputFolder "DriftReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" # ───────────────────────────────────────────────────────────────────────────── # 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 Compliance Drift Detection ║" -ForegroundColor Cyan Write-Host " ║ move2modern.co.uk ║" -ForegroundColor Cyan Write-Host " ╚═══════════════════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" # ───────────────────────────────────────────────────────────────────────────── # 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" Write-Info "A browser sign-in window will open. Sign in with an account that has" Write-Info "the DeviceManagementManagedDevices.Read.All permission." Write-Host "" 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 — RETRIEVE DEVICES # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 3 — Retrieving managed devices" Write-Info "Using -All to ensure all pages are retrieved (large tenants may take a moment)..." Write-Host "" try { $Devices = Get-MgDeviceManagementManagedDevice -All -Property ` Id, DeviceName, ComplianceState, LastSyncDateTime, ` OperatingSystem, OSVersion, UserPrincipalName, EnrolledDateTime ` -ErrorAction Stop Write-Ok "Total managed devices found: $($Devices.Count)" } catch { Write-Host " [XX] Failed to retrieve devices: $_" -ForegroundColor Red Disconnect-MgGraph Read-Host " Press Enter to close" exit 1 } # ───────────────────────────────────────────────────────────────────────────── # STEP 4 — FILTER AND BUILD REPORT # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 4 — Identifying drift" $DriftedDevices = $Devices | Where-Object { $_.ComplianceState -notin @('compliant', 'notApplicable') } $Report = $DriftedDevices | Select-Object @( @{ N = 'DeviceName'; E = { $_.DeviceName } } @{ N = 'UPN'; E = { $_.UserPrincipalName } } @{ N = 'State'; E = { $_.ComplianceState } } @{ N = 'OS'; E = { $_.OperatingSystem } } @{ N = 'OSVersion'; E = { $_.OSVersion } } @{ N = 'LastSync'; E = { $_.LastSyncDateTime } } @{ N = 'DaysSinceSync'; E = { [int]((Get-Date) - [datetime]$_.LastSyncDateTime).TotalDays } } @{ N = 'EnrolledDate'; E = { $_.EnrolledDateTime } } ) # ───────────────────────────────────────────────────────────────────────────── # STEP 5 — CONSOLE OUTPUT # ───────────────────────────────────────────────────────────────────────────── Write-Header "Step 5 — Results" Write-Host "" if ($Report.Count -eq 0) { Write-Host " ✅ No drifted devices found. All managed devices are compliant or not applicable." ` -ForegroundColor Green } else { # Summary line Write-Host (" ⚠ {0} drifted device(s) out of {1} total ({2:0.0}%)" -f ` $Report.Count, $Devices.Count, (($Report.Count / $Devices.Count) * 100)) ` -ForegroundColor Yellow Write-Host "" # Column widths — dynamic, based on actual data, capped for readability $colW = @{ DeviceName = [Math]::Min(30, ($Report | Measure-Object { $_.DeviceName.Length } -Maximum).Maximum + 2) UPN = [Math]::Min(36, ($Report | Measure-Object { $_.UPN.Length } -Maximum).Maximum + 2) State = 18 OS = 10 DaysSync = 12 } # Header row $header = " {0,-$($colW.DeviceName)}{1,-$($colW.UPN)}{2,-$($colW.State)}{3,-$($colW.OS)}{4,-$($colW.DaysSync)}" ` -f 'Device Name', 'User', 'State', 'OS', 'Days Since Sync' $divider = " " + ("─" * ($colW.DeviceName + $colW.UPN + $colW.State + $colW.OS + $colW.DaysSync)) Write-Host $header -ForegroundColor White Write-Host $divider -ForegroundColor DarkGray # Data rows — colour by state foreach ($Device in $Report | Sort-Object State, DeviceName) { $stateColor = switch ($Device.State) { 'noncompliant' { 'Red' } 'error' { 'Red' } 'unknown' { 'Yellow' } 'inGracePeriod' { 'Cyan' } default { 'White' } } $row = " {0,-$($colW.DeviceName)}{1,-$($colW.UPN)}{2,-$($colW.State)}{3,-$($colW.OS)}{4,-$($colW.DaysSync)}" ` -f ` ($Device.DeviceName | Select-Object -First 1), ($Device.UPN | Select-Object -First 1), $Device.State, $Device.OS, $Device.DaysSinceSync Write-Host $row -ForegroundColor $stateColor } Write-Host $divider -ForegroundColor DarkGray Write-Host "" # State breakdown Write-Host " Breakdown by state:" -ForegroundColor White $Report | Group-Object State | Sort-Object Count -Descending | ForEach-Object { Write-Host (" {0,-20} {1} device(s)" -f $_.Name, $_.Count) -ForegroundColor Gray } } # ───────────────────────────────────────────────────────────────────────────── # STEP 6 — SAVE CSV # ───────────────────────────────────────────────────────────────────────────── Write-Host "" Write-Header "Step 6 — Saving report" # Create output folder if it doesn't exist if (-not (Test-Path $OutputFolder)) { New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null Write-Ok "Created folder: $OutputFolder" } else { Write-Info "Output folder: $OutputFolder" } if ($Report.Count -eq 0) { Write-Info "No drifted devices to save." } else { try { $Report | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8 -ErrorAction Stop Write-Ok "CSV saved to: $OutputFile" } catch { Write-Host " [XX] Could not save CSV: $_" -ForegroundColor Red } } # ───────────────────────────────────────────────────────────────────────────── # 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"