Training module · Draft
Attribute-Driven Migration, Module 4: Move Group Policy as Code
Turn Group Policy analytics into a per-setting move plan, keep settings catalog policies as reviewed files, and create only what a dry run showed, with the least permission that works
Status — Draft
This page is still being written. Expect gaps and changes; it is not part of the workbook's production guidance. Last reviewed 2026-11-07.
Learning path: Building the Attribute-Driven Migration · Module 4 of 9 · 50 minutes · Guide Gate 0, Gate 5 and Gate 8 · Workbook Chapter 06
Watch: the narrated deck for this module: Deck · ▶ Watch (about 15 minutes, captions) · MP4 · Script · Captions.
Also: the chapter’s narrated tour, about 11 minutes: ▶ Watch.
Unit 1: Introduction
5 minutes, with the objectives and prerequisites
The OU tree scoped Group Policy, and Group Policy inheritance retires with it. Gate 0 splits every GPO by track, and its Validate step runs VK-INT-07 to show which GPOs Group Policy analytics has imported and how ready they are. Gate 5 moves each user-configuration setting to exactly one owner (computer settings go through the Intune guide’s Gate 3, which is where this module’s device settings land). Gate 8 cuts over by disabling GPO links, never deleting them. This module is the code between those gates: a plan per setting, a library of reviewed files, and a sync that creates only what a dry run showed.
The federal requirement. Moving Group Policy to Intune changes the baseline of every device, so the plan, the library and the sync record are configuration-management evidence. From Chapter 06’s table:
| Requirement | KSI | Evidence the ISSO keeps |
|---|---|---|
| CM-2 Baseline Configuration; CA-7: every in-scope GPO imported and ready, or listed | KSI-MLA-EVC | VK-INT-07’s Results/ValidationResults.json |
| CM-2; CM-6: each GPO setting has a disposition, and conflicts are shown | KSI-SVC-ACM | The move plan, saved with the pull request |
| CM-3; CM-6: one owner and one decision record per setting | KSI-CMT-VTD | The library in Git, and the decision records |
| CM-3; AC-6: the sync shows what it would create, then writes only that, with read-only scope for the dry run | KSI-CMT-LMC; KSI-CMT-RMV; KSI-IAM-ELP | out/sync-record.json |
| CM-3; CA-7: a library that differs from the tenant stops the release | KSI-MLA-EVC; KSI-CMT-VTD | The run log and the pull request |
| SA-11; SR-3: Microsoft’s sample is pinned and its behaviour tested | KSI-SCR-MIT; KSI-CMT-VTD | snapshot.json and MicrosoftSamples.Tests.ps1 |
Key Security Indicators are FedRAMP 20x indicators for cloud service providers. The agency authorizes its own system and lines this evidence up with what its providers report against them; it doesn’t report KSIs itself (Book 00, Appendix B). Which output supports which control is the program’s reading (design guidance).
Learning objectives
In this module, you’ll:
- Explain what VK-INT-07 checks per GPO, and what the move plan adds per setting.
- Name the plan’s dispositions, and what happens to a
Conflict. - Describe a library file: the policy body Graph receives, and the record around it.
- Run the dry run and the apply, and read the sync record.
- Explain why a
Driftstops the release, and why the sync never sends PATCH or DELETE.
Prerequisites
- Module 2, for how VK-INT-07 is built.
- Optional: Microsoft Automation, Module 3, for the settings catalog and Intune’s v1.0 and beta APIs.
- PowerShell 7.1 or later and Pester 5.
Unit 2: The gate and the move plan
8 minutes
The operator side is already written: Book 05 runs the GPO sunset, and the Intune track guide turns it into steps. VK-INT-07 works per GPO. Get-GpcMovePlan works per setting and gives each one a disposition. Microsoft’s import sample creates a policy with one POST and never looks at the tenant first. Read:
- The guide’s Gate 0, step 5 and its Validate step, and Gate 5, step 5.
- Chapter 06: where this sits, for the two rules that shape the code: one owning policy per setting, and build unassigned.
- Chapter 06: what Microsoft’s samples leave to you.
- Read: the gate and the move plan.
Unit 3: The library, the linter and the sync
8 minutes
Each policy is a file: the body Graph receives, exactly as reviewed, with its owner, its source GPOs and a decision record per setting. The sync reads the tenant, normalizes both sides and gives each policy one action: Create, NoChange, Drift or Duplicate. It asks for Read.All for a dry run and ReadWrite.All only with -Apply. Read:
Unit 4: Exercise: Lab 1 (fixtures, no tenant)
22 minutes
Every command in this lab, in order, from examples/group-policy-as-code. Each step below says what it does and what to look for.
# Step 1: Run the gate
Set-Location ../validation-kit
Import-Module ./powershell/ValidationKit/ValidationKit.psd1
$ctx = Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf '2026-10-29T00:00:00Z' -IncludeBeta
(Invoke-VkCheck -CheckId VK-INT-07 -Context $ctx).Details | Format-Table Severity, Object, Issue
# Step 2: Run Microsoft's import sample twice against the fake Graph
Set-Location ../group-policy-as-code
Import-Module ./powershell/GroupPolicyAsCode.psm1
. ./tests/MicrosoftSampleHarness.ps1
New-Item -ItemType Directory -Force ./out | Out-Null
(Get-Content ./policies/fmp-sc-201-error-reporting-and-dumps.json -Raw | ConvertFrom-Json).policy |
ConvertTo-Json -Depth 20 | Set-Content ./out/sc-201.json
$g = New-GpcFixtureInvoke -Path ./tests/fixtures/tenant.json
Invoke-MicrosoftSample -Name Import -Graph $g -Answers (Resolve-Path ./out/sc-201.json).Path | Out-Null
Invoke-MicrosoftSample -Name Import -Graph $g -Answers (Resolve-Path ./out/sc-201.json).Path | Out-Null
$g.Policies | ForEach-Object { $_['name'] }
$g.Calls | Format-Table Method, Uri
# Step 3: Build the move plan
$plan = Get-GpcMovePlan -Invoke (New-GpcFixtureInvoke -Path ./tests/fixtures/tenant.json).Invoke
$plan | Format-Table Gpo, Setting, Scope, Disposition
# Step 4: Lint the library against the plan
Test-GpcLibrary -Library (Read-GpcLibrary -Path ./policies) -Plan $plan | Format-List
# Step 5: Run the dry run, then apply
pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -WithPlan
pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -Apply
# Step 5: Run the dry run, then apply
Get-Content ./out/sync-record.json -Raw | ConvertFrom-Json | Format-List Scopes, Created
pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -WithPlan -Cloud usgov
# Step 6: Break something on purpose
$f = './policies/fmp-sc-202-sign-in-and-power.json'
(Get-Content -Raw $f).Replace('power_allowhibernate_0', 'power_allowhibernate_1') | Set-Content -NoNewline $f
pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -WithPlan
# Step 6: Break something on purpose
pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -Apply
# Step 6: Break something on purpose
(Get-Content -Raw $f).Replace('power_allowhibernate_1', 'power_allowhibernate_0') | Set-Content -NoNewline $f
# Step 7: Run the tests
Invoke-Pester ./tests
# Step 8: Clean up
Remove-Item -Recurse -Force ./outYou run the gate, run Microsoft’s sample twice, build the plan, lint the library, sync it, and break it on purpose. Work in examples/group-policy-as-code, in PowerShell 7; step 1 starts in examples/validation-kit and moves back. These are the steps of Chapter 06, Lab 1, which explains each result.
Run the gate. Run VK-INT-07 on the validation kit’s fail fixture:
Set-Location ../validation-kit Import-Module ./powershell/ValidationKit/ValidationKit.psd1 $ctx = Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf '2026-10-29T00:00:00Z' -IncludeBeta (Invoke-VkCheck -CheckId VK-INT-07 -Context $ctx).Details | Format-Table Severity, Object, IssueExpected output:
Severity Object Issue -------- ------ ----- Info FMP-Workstation-Baseline Imported GPO Finding Workstation-Drive-Maps Targeted in AD, migration readiness 'partial': settings Intune can't take over Finding Workstation-Scripts Targeted in AD, migration readiness 'error': settings Intune can't take over Warn Workstation-Empty Targeted in AD, readiness notApplicable: confirm where its settings go Finding Workstation-Printers In scope, but not imported into Group Policy analyticsRun Microsoft’s import sample twice against the fake Graph.
Set-Location ../group-policy-as-code Import-Module ./powershell/GroupPolicyAsCode.psm1 . ./tests/MicrosoftSampleHarness.ps1 New-Item -ItemType Directory -Force ./out | Out-Null (Get-Content ./policies/fmp-sc-201-error-reporting-and-dumps.json -Raw | ConvertFrom-Json).policy | ConvertTo-Json -Depth 20 | Set-Content ./out/sc-201.json $g = New-GpcFixtureInvoke -Path ./tests/fixtures/tenant.json Invoke-MicrosoftSample -Name Import -Graph $g -Answers (Resolve-Path ./out/sc-201.json).Path | Out-Null Invoke-MicrosoftSample -Name Import -Graph $g -Answers (Resolve-Path ./out/sc-201.json).Path | Out-Null $g.Policies | ForEach-Object { $_['name'] } $g.Calls | Format-Table Method, UriExpected:
FMP-SC-201-Error-reporting-and-dumpsis listed twice, and the calls are twoPOSTs tohttps://graph.microsoft.com/beta/deviceManagement/configurationPolicieswith noGET. The sample never looked.Build the move plan.
$plan = Get-GpcMovePlan -Invoke (New-GpcFixtureInvoke -Path ./tests/fixtures/tenant.json).Invoke $plan | Format-Table Gpo, Setting, Scope, DispositionExpected: eleven rows from three GPOs. Five are
Candidate.Allow hibernateis aConflicton two rows, the firewall rule isEndpointSecurity, two rows areNotSupportedand one isDeprecated.Lint the library against the plan.
Test-GpcLibrary -Library (Read-GpcLibrary -Path ./policies) -Plan $plan | Format-ListExpected: one row,
InfoGPC-08: the hibernate setting has conflicting GPO values, and decisionDR-0105picks the value. (DR-0105is a decision record, not a dynamic-rule lint rule like Module 3’sDR-05.)Run the dry run, then apply.
pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -WithPlan pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -ApplyExpected: the dry run ends
Create FMP-SC-201-Error-reporting-and-dumpsandNoChange FMP-SC-202-Sign-in-and-power; the apply endsCreated FMP-SC-201-Error-reporting-and-dumps. Both exit 0. Read the record, then ask for a cloud name the script doesn’t know:Get-Content ./out/sync-record.json -Raw | ConvertFrom-Json | Format-List Scopes, Created pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -WithPlan -Cloud usgovExpected:
Scopes : {DeviceManagementConfiguration.ReadWrite.All}andCreated : {00000000-0000-4000-9000-000000000001}; then exit 1, becauseusgovisn’t one ofgcc,global,gcchigh,dod.Break something on purpose. The first two commands change the hibernate value in one library file; then run the dry run:
$f = './policies/fmp-sc-202-sign-in-and-power.json' (Get-Content -Raw $f).Replace('power_allowhibernate_0', 'power_allowhibernate_1') | Set-Content -NoNewline $f pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -WithPlanExpected: exit 2, with
Drift FMP-SC-202-Sign-in-and-power: changes device_vendor_msft_policy_config_power_allowhibernate. Run it with-Apply:pwsh ./powershell/Invoke-GpcSync.ps1 -FixturePath ./tests/fixtures/tenant.json -ApplyExpected: still exit 2, and nothing is created, not even SC-201. Put the value back:
(Get-Content -Raw $f).Replace('power_allowhibernate_1', 'power_allowhibernate_0') | Set-Content -NoNewline $fRun the tests.
Invoke-Pester ./testsExpected:
Tests Passed: 46.Clean up.
Remove-Item -Recurse -Force ./out
Check your work: for any GPO setting, you can say its disposition, which library file owns it and which decision record picks its value, and what the sync would do with it.
Unit 5: Exercise: Lab 2 (your test tenant)
Not timed. Not run by the authors.
Import a test GPO into Group Policy analytics, run VK-INT-07 live as a read-only operator, build the move plan live, dry-run and apply as a Policy and Profile Manager, compare the result with what Group Policy analytics’ Migrate builds, and drill drift by changing a value in the admin center. Follow Chapter 06, Lab 2.
- You need: a test tenant with Intune licenses and a test GPO export, never production. In GCC High and DoD, use
-Cloud gcchighor-Cloud dod. - Back it out: step 8 deletes the created policies by the IDs in
out/apply/sync-record.json, and the Migrate comparison policy; then delete the imported GPO, as the chapter’s clean-up does.
Unit 6: Knowledge check
5 minutes
- Microsoft’s import sample runs twice with the same file. What happens?
- The second run finds the policy and does nothing.
- Two policies with the same name are created, because the sample never reads the tenant.
- The second run updates the first policy.
- Two GPOs set the same Intune setting to different values. What disposition does the plan give it?
Candidate.Conflict, and a decision record picks the value.NotSupported.
- One library file differs from the tenant’s copy, and you run the sync with
-Apply. What does it create?- Only the policies marked
Create. - Nothing: it exits 2 and writes nothing while any policy is
Drift. - It updates the drifted policy.
- Only the policies marked
- Which permission does a dry run ask for?
DeviceManagementConfiguration.Read.All;ReadWrite.Allonly with-Apply.DeviceManagementConfiguration.ReadWrite.All, always.- None.
- b. See what Microsoft’s samples leave to you and step 2 of the lab.
- b. See Read: the gate and the move plan and steps 3 and 4 of the lab.
- b. See Diff, then create and step 6 of the lab.
- a. See what Microsoft’s samples leave to you and Diff, then create.
Unit 7: Summary
2 minutes
You read Group Policy analytics into a plan per setting, kept the policies as reviewed files, and saw the sync create only what its dry run showed, and nothing at all when the library and the tenant disagree.
Evidence an ISSO keeps from this module: VK-INT-07’s run, the move plan with each pull request, the library and its decision records in Git, sync-record.json with its scopes, file hashes and created IDs, and in Lab 2 the Intune audit log for the import, the creation and the drift drill. At deployment, Chapter 10 adds the GPO link states before and after, for Gates 5 and 8 (evidence the ISSO keeps). Before shipping, work through the Chapter 06 checklist.