~/2026/04/01/intune-migrating-from-group-policy-to-cloud-native-endpoint-management.md

Intune: Migrating From Group Policy to Cloud-Native Management

---
author: 
date: 
read: 7 min
in:   [engineering, strategy]
tags: [intune, gpo, windows, active-directory]
---

$ grep -n '^#' post.md

Group Policy has been the backbone of Windows management for two decades, and it still works, which is exactly why moving off it is uncomfortable. Nobody migrates away from something broken by comparison; they migrate away from something that quietly does its job while the rest of the environment moves toward devices that never touch the corporate network at all. That was the real driver in the environments I've helped move to Intune: not that GPO failed, but that it only reaches machines that can see a domain controller, and an increasing share of a modern fleet never does.

This is the sequence I use now, with the scripts that go with each step.

Inventory before you touch anything

The first mistake is starting in Intune and working backward. The right first step is exporting every GPO that's actually linked, not the ones sitting unlinked in the GPO store from a project three reorganizations ago, and cataloging what each setting does, who it affects, and whether it's still relevant. Every estate I've done this in carried dead weight: settings for a VPN client that was decommissioned, drive mappings to a file server that moved, a login script nobody remembers writing. Migrating dead policy is wasted work. Cutting it first shrinks the real migration to something manageable.

The script below does the mechanical half. It needs the GroupPolicy module (RSAT Group Policy Management Tools) and read access to the domain's GPOs. For every GPO it writes the XML report that Intune's Group Policy analytics imports, then builds a CSV with link status, which halves are disabled, last modification time, and whether the XML is over the 4 MB import limit.

powershell
<#
.SYNOPSIS
    Exports every GPO in the domain to XML and builds a link inventory CSV.
.DESCRIPTION
    Uses Get-GPO -All and Get-GPOReport -ReportType Xml to write one XML report
    per GPO (the format Intune Group Policy analytics imports), then reads each
    report's LinksTo entries to record where the GPO is linked and whether the
    link is enabled. Flags unlinked GPOs, GPOs with all settings disabled, and
    reports over the 4 MB Group Policy analytics import limit.
.PARAMETER OutputFolder
    Folder for the XML reports and gpo-inventory.csv.
.PARAMETER Domain
    FQDN of the domain to read. Defaults to the current user's domain.
.EXAMPLE
    .\Export-GpoInventory.ps1 -OutputFolder C:\GpoExport
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2026-04-01)
    Requires: GroupPolicy module (RSAT), read access to GPOs.
#>

[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [string]$OutputFolder,

    [Parameter()]
    [string]$Domain = $env:USERDNSDOMAIN
)

Import-Module GroupPolicy
New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null

$maxBytes = 4MB
$inventory = foreach ($gpo in Get-GPO -All -Domain $Domain) {
    $safeName = $gpo.DisplayName -replace '[\\/:*?"<>|]', '_'
    # Display names aren't guaranteed unique, so the GUID keeps each file distinct.
    $xmlPath = Join-Path -Path $OutputFolder -ChildPath "$safeName-$($gpo.Id).xml"

    Get-GPOReport -Guid $gpo.Id -ReportType Xml -Domain $Domain -Path $xmlPath
    [xml]$report = Get-Content -Path $xmlPath -Raw

    # Each LinksTo element is one link (SOMPath is the OU or domain path).
    $links = @($report.GPO.LinksTo)
    $enabledLinks = @($links | Where-Object { $_.Enabled -eq 'true' })
    $size = (Get-Item -Path $xmlPath).Length

    $candidate = 'Migrate or retire'
    if ($enabledLinks.Count -eq 0) {
        $candidate = 'Unlinked: review for deletion'
    } elseif ($gpo.GpoStatus -eq 'AllSettingsDisabled') {
        $candidate = 'All settings disabled'
    }

    [PSCustomObject]@{
        Name             = $gpo.DisplayName
        Id               = $gpo.Id
        GpoStatus        = $gpo.GpoStatus
        ModificationTime = $gpo.ModificationTime
        LinkCount        = $links.Count
        EnabledLinks     = $enabledLinks.Count
        LinkedTo         = ($enabledLinks | ForEach-Object { $_.SOMPath }) -join '; '
        ReportKB         = [math]::Round($size / 1KB, 1)
        OverImportLimit  = $size -gt $maxBytes
        Candidate        = $candidate
    }
}

$csvPath = Join-Path -Path $OutputFolder -ChildPath 'gpo-inventory.csv'
$inventory | Sort-Object -Property EnabledLinks, Name | Export-Csv -Path $csvPath -NoTypeInformation -Encoding utf8
$inventory | Group-Object -Property Candidate | Select-Object -Property Name, Count | Format-Table -AutoSize
Write-Host "Wrote $(@($inventory).Count) reports and $csvPath"

Typical console output:

text
Name                          Count
----                          -----
Migrate or retire                58
Unlinked: review for deletion    23
All settings disabled             6

Wrote 87 reports and C:\GpoExport\gpo-inventory.csv

The LinksTo, SOMPath and Enabled element names come from the GPMC XML report format; open one exported file and check them before you trust the counts. The CSV is where the real work starts: every row in the "Migrate or retire" bucket gets an owner and a decision before anything is built in Intune.

Let Group Policy analytics do the first pass

Intune's Group Policy analytics (Devices > Manage devices > Group Policy analytics > Import) takes those XML files and tells you, per GPO, what percentage of settings has an MDM equivalent. Microsoft documents GPMC's Save Report as XML as the export method; if a file produced by Get-GPOReport won't import, re-save that GPO from GPMC. The rules worth knowing before you start:

  • A single GPO XML must be under 4 MB and correctly Unicode-encoded, or the import fails. The inventory script flags oversize files.
  • You can import several files at once. The list view shows MDM Support (the percentage), Unknown Settings (settings in CSPs the tool can't parse), and Targeted in AD, which says whether the GPO is linked to an OU.
  • Drilling into a GPO shows each setting's CSP Name and CSP Mapping, which is the OMA-URI path. That column is useful even when you don't use the migration feature, because it hands you the exact path for a custom profile.
  • The migration readiness report (Reports > Device management > Group policy analytics) sorts settings into ready for migration, not supported, and deprecated.
  • The parser covers the Policy, PassportForWork, BitLocker, Firewall and AppLocker CSPs plus Group Policy Preferences. Known issue: only English-language non-ADMX settings are analyzed correctly, so a GPO authored in another language reports a misleading percentage.

The Migrate button then builds a Settings catalog profile from the settings you tick. Microsoft calls it best effort, and the details matter: AppLocker and firewall settings can't be migrated this way (build those in Endpoint security), some older Office and Chrome settings map to a newer equivalent rather than the same setting, and if two imported GPOs set the same setting to different values, the wizard stops with "Conflicts are detected for the following settings" until you pick one.

Settings catalog versus custom OMA-URI

Once you know what needs to move, most of it maps onto the Settings catalog, which covers most of what a configuration profile can express. A few things have their own policy types instead: security baselines, Windows Update rings, and BitLocker (under Endpoint security > Disk encryption). The catalog covers more ground every release, and I'd always rather use it than hand-roll a custom OMA-URI profile, because Microsoft documents and tests the catalog paths and you're on your own with a raw CSP. The friction shows up in the long tail: printer deployment logic, application-specific registry keys tied to a piece of line-of-business software, and login scripts doing things GPO could do natively but nobody ever moved off a script. Those need Win32 app deployments, PowerShell scripts or Remediations run through Intune, or in a few cases just get retired because the thing they configured stopped mattering years ago. If a login script really was a repair job, a Remediations detect and remediate pair is usually the better fit; the BitLocker remediation post shows the shape of one.

Running both in parallel is not optional

I've never seen a clean cutover work, and I stopped trying to force one. Two different parallel models get lumped together here, and it's worth being precise:

  • Co-management is Configuration Manager and Intune managing the same device, with workloads moved one at a time: compliance policies, Windows Update policies, resource access, Endpoint Protection, device configuration, Office Click-to-Run apps and client apps. Each workload has a slider, and pilot collections let you move a subset first. This is the cleanest way to shift ownership if you run ConfigMgr.
  • Group Policy plus Intune on hybrid-joined devices has no slider. Both engines apply whatever they're given, so ownership is something you enforce yourself, setting area by setting area.

Either way the order I use is the same: Windows Update first (low risk, easy to verify, and Update rings replace a WSUS GPO cleanly), then device restrictions and security settings, then the application and script-based settings last. Each area gets its own pilot group, verify, expand cycle. After moving an area, forcing a check-in on the pilot group shortens the verify step considerably; the fleet-wide sync script does that through Graph. Rushing this is how you end up with conflicting policies fighting silently, which is worse than either source acting alone, because troubleshooting means checking two consoles instead of one.

Conflict resolution and the settings you'll fight over

The earlier version of this section said Windows generally lets GPO win. That's not what Microsoft documents. When the same setting is configured by both Group Policy and MDM, and the setting isn't covered by the MDMWinsOverGP policy, Microsoft's words are that "there will be a race condition and no guarantee which one wins." Which is worse than a predictable loser, and it explains the afternoons lost to a setting that applies on one reboot and not the next.

MDMWinsOverGP (ControlPolicyConflict in the Policy CSP, Windows 10 1803 and later) changes that for settings in scope. With it set to 1, any MDM policy that has an equivalent Group Policy blocks the GP version. I deploy it as a custom OMA-URI setting to the same pilot groups as the first migrated profiles:

FieldValue
OMA-URI./Device/Vendor/MSFT/Policy/Config/ControlPolicyConflict/MDMWinsOverGP
Data typeInteger
Value1 (MDM policy is used, GP policy is blocked); default 0

The limits are the important part. It applies only to policies in the Policy CSP. Settings defined in other CSPs (Microsoft's example is the Defender CSP) aren't covered, so for those the guidance is simply not to configure the same setting in both places. The MDM Diagnostic report lists which GP settings were blocked because an MDM equivalent exists, which is the quickest way to prove what's happening on a disputed device.

To gather that evidence in one pass, I run this on the device. It produces the Group Policy result report, the MDM diagnostics archive, and a CSV of the values MDM has delivered under HKLM\SOFTWARE\Microsoft\PolicyManager\current\device, along with whether MDMWinsOverGP has arrived:

powershell
<#
.SYNOPSIS
    Collects Group Policy and MDM policy evidence from one Windows device.
.DESCRIPTION
    Runs gpresult for the computer scope (HTML), runs mdmdiagnosticstool to
    produce the MDM diagnostics archive, lists the MDM-delivered policy values
    under HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device, and reports
    whether ControlPolicyConflict/MDMWinsOverGP is present.
.PARAMETER OutputFolder
    Folder for the reports. Defaults to C:\Users\Public\Documents\PolicyEvidence.
.EXAMPLE
    .\Get-PolicySourceEvidence.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2026-04-01)
    Requires: Elevated PowerShell on the device.
#>

[CmdletBinding()]
param (
    [Parameter()]
    [string]$OutputFolder = "C:\Users\Public\Documents\PolicyEvidence"
)

New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null

# Group Policy resultant set for the computer.
$gpReport = Join-Path -Path $OutputFolder -ChildPath "gpresult-computer.html"
& gpresult.exe /scope computer /h $gpReport /f | Out-Null

# MDM diagnostics archive (includes MDMDiagHtmlReport.html and the admin event log).
$mdmZip = Join-Path -Path $OutputFolder -ChildPath "MDMDiagReport.zip"
& mdmdiagnosticstool.exe -area "DeviceEnrollment;DeviceProvisioning" -zip $mdmZip | Out-Null

# Values MDM has delivered, one row per area and setting.
$root = "HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device"
$rows = foreach ($area in Get-ChildItem -Path $root -ErrorAction SilentlyContinue) {
    $values = Get-ItemProperty -Path $area.PSPath
    foreach ($property in $values.PSObject.Properties) {
        if ($property.Name -notlike 'PS*') {
            [PSCustomObject]@{
                Area    = $area.PSChildName
                Setting = $property.Name
                Value   = $property.Value
            }
        }
    }
}
$csv = Join-Path -Path $OutputFolder -ChildPath "mdm-policies.csv"
$rows | Export-Csv -Path $csv -NoTypeInformation -Encoding utf8

$conflict = Get-ItemProperty -Path "$root\ControlPolicyConflict" -Name "MDMWinsOverGP" -ErrorAction SilentlyContinue
$mdmWins = if ($conflict) { $conflict.MDMWinsOverGP } else { "not delivered" }

Write-Host "MDM policy values : $(@($rows).Count) in $(@($rows | Select-Object -Property Area -Unique).Count) areas -> $csv"
Write-Host "MDMWinsOverGP     : $mdmWins"
Write-Host "gpresult          : $gpReport"
Write-Host "MDM diagnostics   : $mdmZip"

Compare the areas in mdm-policies.csv with the computer settings in the gpresult report. Any setting configured in both, outside the Policy CSP, is one you need to remove from one side.

What made the difference in practice

The migrations that went smoothly were the ones where I resisted the urge to modernize everything at once. Moving a legacy GPO setting into Intune exactly as-is, verifying it, and only then considering whether a cloud-native equivalent (Windows Update rings instead of a WSUS GPO, for instance) is actually a better fit kept the risk contained to one variable at a time. The migrations that went badly were the ones where "let's finally fix this while we're in there" turned one change into three, and when something broke nobody could say which of the three caused it.

The last step is the one people skip: once an area is fully owned by Intune and verified, unlink the GPO rather than leaving it in place "just in case." A GPO that still applies is a second source of truth, and the whole point was to have one.

References