Training module · Draft
Attribute-Driven Migration, Module 2: Write a Validation Kit Check
Build and read VK-SYNC-02, the check that soft match and hard-match takeover are blocked, and learn why a read-only check still has to ask for the least privilege it can
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 2 of 9 · 45 minutes · Guide Gate 3 · Workbook Chapter 03
Watch: the narrated deck for this module: Deck · ▶ Watch (about 13 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
Every gate in the guide ends with a Validate step, and most of them name a validation kit check. The kit is the executable form of those steps. This module teaches how a check is built, through one new check. Before any AD user is synchronized into a tenant that already has cloud accounts, a soft or hard match left open lets an AD object take over a cloud account. VK-SYNC-02 asks whether both blocks are on, and if not, whether an approved window says why. The guide lists it as Gate 3’s optional match-blocking check in Where the kits stop.
The federal requirement. A check is monitoring evidence only if it can’t change what it reads and can’t pass when it didn’t look. From Chapter 03’s table:
| Requirement | KSI | Evidence the ISSO keeps |
|---|---|---|
| CM-6; CA-7; AC-2: an AD object can’t take over a cloud account unnoticed | KSI-MLA-EVC | The run’s Results/ValidationResults.json |
| CM-6: a deviation is approved, documented and has an end | KSI-MLA-EVC | The window date in the run’s options, and the change record |
| AC-6: no standing Global Administrator identity for a scheduled check | KSI-IAM-ELP | The run’s evidence block, which says the read came from an export; the kit’s catalog |
| SA-11; CM-7 Least Functionality: the kit can only read | KSI-CMT-VTD | The repository’s test results, including Safety.Tests.ps1 |
| SI-7 Software, Firmware, and Information Integrity; AU-3 (Lab 2) | KSI-SVC-VRI | ValidationManifest.json, with a SHA-256 for every file of the run |
| CM-3; CM-5: the remediation is a write under change control, never the check | KSI-CMT-LMC | The change record the finding opens |
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 the parts of a check: catalog entry, function, rows and outcome, fixtures and tests.
- Explain Finding, Warn and Info, and why a missing value reads as off.
- Explain why VK-SYNC-02 is optional and prefers an export to a live read.
- Run the check on fixtures and on an export, and see Pass, Fail and Warn.
- Explain what the safety tests refuse, and why a new check inherits them.
Prerequisites
- Module 1, and Microsoft Automation, Module 1 for permissions and roles.
- PowerShell 7 and Pester 5.5 or later. You read PowerShell, and you know Pester’s
DescribeandIt.
Unit 2: Why this check, and the lesson in its catalog entry
8 minutes
The read is GET /directory/onPremisesSynchronization, and its permission looks harmless. But Learn lists no application permission for it, and for delegated access only Global Administrator. A read-only check, run live, would need the most powerful role in the tenant. So VK-SYNC-02 is optional, prefers an export someone already signed in can save, and names the role in its catalog entry. Read:
- Chapter 03: why this check.
- The catalog entry, and the lesson in it.
- The guide’s Gate 3, step 4: block soft and hard match outside a planned window.
Unit 3: Rows, outcome and safety
8 minutes
A Finding fails the check. A Warn needs a person but isn’t a failure: here, a block that is off inside an approved window, until the window’s last day. An Info row is context and isn’t counted. A missing value reads as off, so an unexpected response produces a Finding, not a silent Pass. The safety tests fail the build on any state-changing command anywhere in the kit. Read:
- Rows and outcome.
- Fixtures and tests.
- The safety tests, including where the remediation goes instead.
Unit 4: Exercise: Lab 1 (fixtures, no tenant)
17 minutes
Every command in this lab, in order, from examples/validation-kit. Each step below says what it does and what to look for.
# Step 1: Run the check against the fail fixture
Import-Module ./powershell/ValidationKit -Force
$r = Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-10-30)
$r | Select-Object CheckId, Status, Count
$r.Details | Format-Table Severity, Issue
$r.Evidence.Queries | Format-Table Source, Query
# Step 2: Open a matching window until October 31, then run on the day after it closes
(Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-10-30 -Option @{ MatchWindowOpenUntil = '2026-10-31' })).Status
(Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-11-01 -Option @{ MatchWindowOpenUntil = '2026-10-31' })).Status
# Step 3: Feed it an export
New-Item -ItemType Directory -Force -Path ./inputs | Out-Null
Set-Content -Path ./inputs/onprem-sync-export.json -Value '{"value":[{"id":"x","features":{"blockSoftMatchEnabled":true,"blockCloudObjectTakeoverThroughHardMatchEnabled":false}}]}'
$r = Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -InputPath ./inputs -AsOf 2026-10-30)
$r | Select-Object CheckId, Status, Count
$r.Evidence.Queries | Format-Table Source, Query
# Step 4: See what a live run would ask for
Get-VkRequiredScope -Checks (Get-VkCheckCatalog -CheckId VK-SYNC-02)
(Get-VkCheckCatalog -CheckId VK-SYNC-02).DelegatedRole
# Step 5: Confirm it isn't in a default run
(Get-VkCheckCatalog).Id -contains 'VK-SYNC-02'
# Step 6: Run the whole kit's tests
pwsh -NoProfile -Command "Invoke-Pester -Path tests/powershell"
# Step 7: Make it fail for the right reason
$f = './powershell/ValidationKit/Checks/SYNC.ps1'
(Get-Content -Raw $f).Replace("Name = 'blockSoftMatchEnabled'", "Name = 'blockSoftMatch'") | Set-Content -NoNewline $f
Import-Module ./powershell/ValidationKit -Force
(Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-10-30)).Status
(Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/pass -AsOf 2026-10-30)).Status
# Step 7: Make it fail for the right reason
(Get-Content -Raw $f).Replace("Name = 'blockSoftMatch'", "Name = 'blockSoftMatchEnabled'") | Set-Content -NoNewline $f
Import-Module ./powershell/ValidationKit -Force
(Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/pass -AsOf 2026-10-30)).Status
# Step 8: Clean up
Remove-Item -Recurse -Force ./inputsYou run the check on its fail fixture, inside and after a matching window, and on an export; you see what a live run would ask for, run the kit’s tests, then break the check on purpose. Work in examples/validation-kit, in PowerShell 7. Nothing here signs in. These are the steps of Chapter 03, Lab 1, which explains each result.
Run the check against the fail fixture.
Import-Module ./powershell/ValidationKit -Force $r = Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-10-30) $r | Select-Object CheckId, Status, Count $r.Details | Format-Table Severity, Issue $r.Evidence.Queries | Format-Table Source, QueryExpected output:
CheckId Status Count ------- ------ ----- VK-SYNC-02 Fail 2 Severity Issue -------- ----- Finding Soft match is not blocked Finding Cloud object takeover through hard match is not blocked Info Soft match on UPN is on; it cannot be turned off once enabled Source Query ------ ----- Graph (fixture) fixture:onPremisesSynchronization.json <= /v1.0/directory/onPremisesSynchronizationOpen a matching window until October 31, then run on the day after it closes.
(Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-10-30 -Option @{ MatchWindowOpenUntil = '2026-10-31' })).Status (Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-11-01 -Option @{ MatchWindowOpenUntil = '2026-10-31' })).StatusExpected:
Warn, thenFail. Nobody edited anything.Feed it an export. The first two commands make an
inputsfolder with an export that blocks soft match but not hard-match takeover.New-Item -ItemType Directory -Force -Path ./inputs | Out-Null Set-Content -Path ./inputs/onprem-sync-export.json -Value '{"value":[{"id":"x","features":{"blockSoftMatchEnabled":true,"blockCloudObjectTakeoverThroughHardMatchEnabled":false}}]}' $r = Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -InputPath ./inputs -AsOf 2026-10-30) $r | Select-Object CheckId, Status, Count $r.Evidence.Queries | Format-Table Source, QueryExpected:
VK-SYNC-02 Fail 1, and the evidence’s source isInput, queryonprem-sync-export.json.See what a live run would ask for.
Get-VkRequiredScope -Checks (Get-VkCheckCatalog -CheckId VK-SYNC-02) (Get-VkCheckCatalog -CheckId VK-SYNC-02).DelegatedRoleExpected output:
OnPremDirectorySynchronization.Read.All Global Administrator (the only role Learn lists; delegated only, no application permission)Confirm it isn’t in a default run.
(Get-VkCheckCatalog).Id -contains 'VK-SYNC-02'Expected:
False.Run the whole kit’s tests.
pwsh -NoProfile -Command "Invoke-Pester -Path tests/powershell"Expected:
Tests Passed: 297, Failed: 0.Make it fail for the right reason. The first two commands change your copy of the check to read
blockSoftMatch, a property that doesn’t exist; the next three reload the module and run the fail and pass fixtures.$f = './powershell/ValidationKit/Checks/SYNC.ps1' (Get-Content -Raw $f).Replace("Name = 'blockSoftMatchEnabled'", "Name = 'blockSoftMatch'") | Set-Content -NoNewline $f Import-Module ./powershell/ValidationKit -Force (Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/fail -AsOf 2026-10-30)).Status (Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/pass -AsOf 2026-10-30)).StatusExpected:
Fail, thenFail. The pass fixture fails too, because the missing value reads as off. Put it back and run the pass fixture again:(Get-Content -Raw $f).Replace("Name = 'blockSoftMatch'", "Name = 'blockSoftMatchEnabled'") | Set-Content -NoNewline $f Import-Module ./powershell/ValidationKit -Force (Invoke-VkCheck -CheckId VK-SYNC-02 -Context (Get-VkRunContext -FixturePath ./tests/fixtures/pass -AsOf 2026-10-30)).StatusExpected:
Pass.Clean up.
Remove-Item -Recurse -Force ./inputs
Check your work: for any state of the two blocks and any window date, you can say what the check reports and why.
Unit 5: Exercise: Lab 2 (your test tenant)
Not timed. Not run by the authors.
A Global Administrator of a test tenant saves the response of the one privileged read to a file; you run the check on that export as any user, compare the result with the export line by line, and delete the export once the run’s evidence is stored. Follow Chapter 03, Lab 2.
- You need: a test tenant with Microsoft Entra Connect or Cloud Sync configured. Only the export step needs Global Administrator; nothing in the kit runs with that role.
- Back it out: step 4 deletes the export. It isn’t evidence by itself; the run’s manifest is.
Unit 6: Knowledge check
5 minutes
- The fixture says soft match isn’t blocked, and an approved matching window ends today. What does VK-SYNC-02 report tomorrow, if nothing changes?
- Warn, because the window was approved.
- Fail, because the day after the window’s last day the same state is a Finding again.
- NotApplicable.
- Why is VK-SYNC-02 left out of a default run?
- It’s in preview.
- Its live read needs Global Administrator, so it runs only when named, and prefers an export.
- It’s slow.
- A response comes back without the
blockSoftMatchEnabledproperty. What does the check report?- Pass, because nothing says the block is off.
- A Finding, because a missing value reads as off.
- An error.
- Your new check seems to need its own HTTP call. What does Chapter 03 tell you to do?
- Add an
Invoke-RestMethodcall with-Method GET. - Add what it needs to the kit’s single data path, where the safety tests already look.
- Mark the check optional.
- Add an
- b. See rows and outcome and step 2 of the lab.
- b. See the catalog entry, and the lesson in it and steps 4 and 5 of the lab.
- b. See rows and outcome and step 7 of the lab.
- b. See the safety tests.
Unit 7: Summary
2 minutes
You built your understanding of a check from its catalog entry to its safety tests, and saw how one check asks for the least privilege it can when its read needs the most powerful role.
Evidence an ISSO keeps from this module: the run’s results, details, summary and ValidationManifest.json in the evidence store; the window date and its change record; the Entra sign-in log for the one privileged export; and the change record for any remediation, which is never made by the check. Before shipping a new check, work through the Chapter 03 checklist.