Training module · Draft
Attribute-Driven Migration, Module 3: Keep Dynamic Rules as Code
Write each dynamic group as a reviewed file with tests, lint it for the guards every user rule needs, show whom a change adds and removes, and pause and resume safely in an incident
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 3 of 9 · 50 minutes · Guide Gate 4 · Workbook Chapter 04
Watch: the narrated deck for this module: Deck · ▶ Watch (about 14 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
Gate 4 replaces “who sits under the Finance branch” with “who carries the Finance code.” A dynamic rule is access policy: one edit can remove hundreds of people at once. So the guide asks for every rule on codes, with guards, tested against named users and shadowed before it grants anything. In this module you keep the rules as files in Git, with tests and a linter, and you see what Learn’s incident samples do when a group was paused on purpose.
The federal requirement. A rule change is a configuration change, and its review is the evidence. From Chapter 04’s table:
| Requirement | KSI | Evidence the ISSO keeps |
|---|---|---|
| AC-2; AC-3: the guards keep disabled accounts and guests out | KSI-IAM-ELP | The rule library and its fixture, in the repository |
| CM-3; CM-4 Impact Analyses: every rule passes its tests, and each change’s adds and removes are listed | KSI-CMT-VTD; KSI-CMT-LMC | The pull request, with out/check-results.json |
| CM-3(2) Testing, Validation, and Documentation of Changes: a broken rule fails the check | KSI-CMT-VTD | The pull request’s check run |
| SA-11; SR-3: Learn’s samples run unchanged and are pinned | KSI-SCR-MIT; KSI-CMT-VTD | snapshot.json and LearnSamples.Tests.ps1 |
| CM-3: the incident pause records each state first, and the resume touches only what it paused (Lab 2) | KSI-CMT-LMC | drill-manifest.json, with the incident or drill record |
| AC-6: the session asks only for what creating groups and evaluating rules need (Lab 2) | KSI-IAM-ELP | The Entra sign-in and audit logs |
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:
- Describe what Learn’s pause and resume samples do well, and what they leave to you.
- Write a rule as one file with an owner and tests.
- Explain the guards the linter enforces, and why it works on the parsed rule.
- Run the pull request check and read its shadow diff.
- Explain how the pause manifest keeps a resume from turning on a group paused on purpose.
Prerequisites
- Module 2, for how the VK-DG checks Gate 4 names are built.
- Optional: Microsoft Automation, Module 2, for groups and membership through Graph.
- PowerShell 7.1 or later and Pester 5, and a Git clone of the repository: step 4 compares the library with
HEAD.
Unit 2: The gate and what Learn’s samples leave to you
8 minutes
Every governed rule uses one of two patterns, requires accountEnabled, excludes guests, and reads codes, never display names. Learn’s dynamic membership processing samples (pause all, all except some or specific collections; resume the noncritical or specific critical ones) pause and resume groups in an incident, but record nothing about what they paused, resume a group that was held for review, and fall back to the worldwide endpoint on an answer they don’t recognize. Read:
- The guide’s Gate 4, steps 1 to 6.
- Chapter 04: where the rules sit.
- Chapter 04: what Learn’s samples leave to you.
Unit 3: The library, the linter, the shadow diff and the pause
10 minutes
Each group is a JSON file with an owner, a critical flag and tests that name members of a fictional population. The linter applies eleven rules on the parsed rule, because a guard inside an -or protects nothing. The pull request check lints, tests and lists each changed rule’s adds and removes. The pause manifest records every collection’s state before an incident pause. Read:
Unit 4: Exercise: Lab 1 (fixtures, no tenant)
20 minutes
Every command in this lab, in order, from examples/dynamic-rules. Each step below says what it does and what to look for.
# Step 1: Run Learn's samples against the fake tenant
. ./tests/LearnSampleHarness.ps1
$pages = '../third_party/dynamic-membership-samples/scripts'
$t = New-FakeTenant -Collections @(
@{ id = '11111111-1111-1111-1111-111111111111'; displayName = 'FIN-LED all members'; kind = 'group'; state = 'On' },
@{ id = '22222222-2222-2222-2222-222222222222'; displayName = 'Held for review'; kind = 'group'; state = 'Paused' })
$answers = 'yes', 'USGov', 'yes', 'yes', 'yes' # run, cloud, groups phase, AU phase, confirm account
Invoke-LearnSample -Page "$pages/powershell-pause-all-dynamic-membership.md" -Tenant $t -Answers $answers | Out-Null
$t.Connected = $null
Invoke-LearnSample -Page "$pages/powershell-resume-noncritical-dynamic-membership.md" -Tenant $t -Answers $answers | Out-Null
$t.Collections.Values | Select-Object displayName, state
# Step 1: Run Learn's samples against the fake tenant
$t = New-FakeTenant -Collections @(
@{ id = '11111111-1111-1111-1111-111111111111'; displayName = 'FIN-LED all members'; kind = 'group'; state = 'On' })
Invoke-LearnSample -Page "$pages/powershell-pause-all-dynamic-membership.md" -Tenant $t -Answers 'yes', 'GCCHigh', 'yes', 'yes', 'yes' | Out-Null
$t.Calls[0].Uri
# Step 2: Lint Book 03's rule table
Import-Module ./powershell/DynamicRules.psm1
Get-Content ./tests/fixtures/book03-patterns.json -Raw | ConvertFrom-Json |
ForEach-Object { Test-DrRule -Rule $_.membershipRule -Kind $_.kind -Group $_.pattern } |
Format-Table Severity, Rule, Group, Issue
# Step 3: Evaluate a rule against the population
$pop = Get-Content ./tests/fixtures/population.json -Raw | ConvertFrom-Json
Get-DrMembers -Population $pop -Rule '(user.accountEnabled -eq true) -and (user.userType -eq "Member") -and (user.extensionAttribute10 -eq "FIN-LED")'
# Step 4: Run the pull request check
pwsh ./powershell/Invoke-DrLibraryCheck.ps1 -BaseRef HEAD
# Step 5: Break a rule on purpose
$f = './rules/fin-led-not-contractors.json'
(Get-Content -Raw $f).Replace(' -and (user.extensionAttribute11 -ne null)', '') | Set-Content -NoNewline $f
# Step 5: Break a rule on purpose
pwsh ./powershell/Invoke-DrLibraryCheck.ps1 -BaseRef HEAD
# Step 5: Break a rule on purpose
(Get-Content -Raw $f).Replace('-ne \"CTR\")"', '-ne \"CTR\") -and (user.extensionAttribute11 -ne null)"') | Set-Content -NoNewline $f
# Step 6: Run the tests
Invoke-Pester ./testsYou run Learn’s samples against a fake tenant, lint Book 03’s rule table, evaluate a rule, run the pull request check and break a rule on purpose. Work in examples/dynamic-rules, in PowerShell 7. These are the steps of Chapter 04, Lab 1, which explains each result.
Run Learn’s samples against the fake tenant. One group is on, and one was paused before the incident.
. ./tests/LearnSampleHarness.ps1 $pages = '../third_party/dynamic-membership-samples/scripts' $t = New-FakeTenant -Collections @( @{ id = '11111111-1111-1111-1111-111111111111'; displayName = 'FIN-LED all members'; kind = 'group'; state = 'On' }, @{ id = '22222222-2222-2222-2222-222222222222'; displayName = 'Held for review'; kind = 'group'; state = 'Paused' }) $answers = 'yes', 'USGov', 'yes', 'yes', 'yes' # run, cloud, groups phase, AU phase, confirm account Invoke-LearnSample -Page "$pages/powershell-pause-all-dynamic-membership.md" -Tenant $t -Answers $answers | Out-Null $t.Connected = $null Invoke-LearnSample -Page "$pages/powershell-resume-noncritical-dynamic-membership.md" -Tenant $t -Answers $answers | Out-Null $t.Collections.Values | Select-Object displayName, stateExpected output:
displayName state ----------- ----- FIN-LED all members On Held for review On“Held for review” was paused on purpose, and the resume turned it on. Now run the pause with
GCCHighas the cloud answer, and read the first call’s address:$t = New-FakeTenant -Collections @( @{ id = '11111111-1111-1111-1111-111111111111'; displayName = 'FIN-LED all members'; kind = 'group'; state = 'On' }) Invoke-LearnSample -Page "$pages/powershell-pause-all-dynamic-membership.md" -Tenant $t -Answers 'yes', 'GCCHigh', 'yes', 'yes', 'yes' | Out-Null $t.Calls[0].UriExpected: the address starts
https://graph.microsoft.com/v1.0/groups, the worldwide host.Lint Book 03’s rule table.
Import-Module ./powershell/DynamicRules.psm1 Get-Content ./tests/fixtures/book03-patterns.json -Raw | ConvertFrom-Json | ForEach-Object { Test-DrRule -Rule $_.membershipRule -Kind $_.kind -Group $_.pattern } | Format-Table Severity, Rule, Group, IssueExpected: two rows,
Finding DR-05 Legal employer(a display-name attribute instead of a coded slot) andInfo DR-03 Direct reports.Evaluate a rule against the population.
$pop = Get-Content ./tests/fixtures/population.json -Raw | ConvertFrom-Json Get-DrMembers -Population $pop -Rule '(user.accountEnabled -eq true) -and (user.userType -eq "Member") -and (user.extensionAttribute10 -eq "FIN-LED")'Expected:
u01,u02,u06,u08,u09. The guards keep outu04(disabled) andu05(a guest).Run the pull request check.
pwsh ./powershell/Invoke-DrLibraryCheck.ps1 -BaseRef HEADExpected:
6 rule(s); 0 finding(s), 0 warning(s); 0 changed file(s), exit 0.Break a rule on purpose. This deletes the empty-slot clause from one rule:
$f = './rules/fin-led-not-contractors.json' (Get-Content -Raw $f).Replace(' -and (user.extensionAttribute11 -ne null)', '') | Set-Content -NoNewline $fRun the check again:
pwsh ./powershell/Invoke-DrLibraryCheck.ps1 -BaseRef HEADExpected: exit 2. The linter notes
DR-10, the test foru06fails (DR-11), and the shadow diff printsfin-led-not-contractors.json (changed): adds u06. Put the clause back:(Get-Content -Raw $f).Replace('-ne \"CTR\")"', '-ne \"CTR\") -and (user.extensionAttribute11 -ne null)"') | Set-Content -NoNewline $fRun the tests.
Invoke-Pester ./testsExpected:
Tests Passed: 70.
Check your work: for any rule change, you can say which guard it needs, whom it adds and removes, and what an incident pause would do to it.
Unit 5: Exercise: Lab 2 (your test tenant)
Not timed. Not run by the authors.
Sign in to the right cloud with the least you need, create the library’s groups paused, compare the offline evaluator with Graph’s beta evaluator for every test, turn processing on and shadow-diff live with VK-DG-04, and drill an incident pause and resume. Follow Chapter 04, Lab 2.
- You need: a test tenant, never production, with fictional users whose slots match the population fixture, and a Groups Administrator for the delegated session.
- Back it out: step 6 deletes the test groups, then lists every dynamic group and unit that isn’t on, to confirm nothing is left paused that the record didn’t pause before.
Unit 6: Knowledge check
5 minutes
- Learn’s resume sample runs after an incident. What happens to a group that was paused for review before the incident?
- It stays paused.
- It’s turned on with the rest.
- The sample asks first.
- Why does the linter work on the parsed rule, not on its text?
- Parsing is faster.
- A guard inside an
-orprotects nothing, and a text search can’t tell the difference. - Learn requires it.
- You delete
-and (user.extensionAttribute11 -ne null)from a rule. What does the pull request check show?- Exit 0, because the rule still parses.
- Exit 2: DR-10, a failed test for
u06, and the shadow diffadds u06. - Exit 1, because the file changed.
- An incident responder resumes collections from the pause manifest. Which ones can it resume?
- Every paused collection in the tenant.
- Only collections this manifest paused; it refuses any other ID.
- Only critical collections.
- b. See what Learn’s samples leave to you and step 1 of the lab.
- b. See Lint.
- b. See the shadow diff and step 5 of the lab.
- b. Critical collections can be resumed at once by ID; the rest wait. See the pause manifest.
Unit 7: Summary
2 minutes
You kept the rules as reviewed files with tests, linted them for the guards, saw whom a one-clause change adds, and saw why an incident pause needs a record of what was paused before.
Evidence an ISSO keeps from this module: the rule library and its commits, each pull request’s check run and check-results.json; in Lab 2, the Entra audit log for the paused creation, the evaluator comparison, VK-DG-04’s shadow diff and the drill manifest. Gate 4’s own evidence, VK-DG-01 to 07 and the restriction decision per unit, is in the guide’s Gate 4. Before shipping a rule change, work through the Chapter 04 checklist.