Training module · Draft
Module 2: Owners, sponsors and the orphan rule
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-09.
Part of the Non-Person Identity learning path. Gates G1 Inventory and G6 Lifecycle. Workbook Chapter 02. About 60 minutes.
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.
Introduction (3 minutes)
An owner can add a credential and act as the app. So owners are a control, not a contact list. The program wants two human owners on every app registration and service principal, a record in the system inventory, and an application owner for every Active Directory account, found before the account is touched.
An identity with no owner is an orphan. The plan’s decision D-NP2 says what happens to it: disable it once the identity governance officer confirms the finding, then allow 30 days for someone to claim it or retire it. Never delete.
In this module you check ownership as code and build the orphan rule as a plan that a reviewer approves by its hash before anything changes.
The federal requirement. Ownership is part of Gate G1, and the orphan rule is part of Gate G6. The guide’s Validate (G1 Inventory) and Validate (G6 Lifecycle) steps name AC-2, AC-2(3), IA-4 and CM-8, among others. The workbook chapter’s Federal requirements and evidence table has a row for every lab step.
| Requirement | KSI | Evidence |
|---|---|---|
| AC-2 Account Management (account managers) | KSI-IAM-SNU | ownership.json; findings.json, NPI-OWN-01 to -05, -08 and -09 |
| IA-4 Identifier Management; CM-8 System Component Inventory | KSI-IAM-ELP, KSI-IAM-JIT (IA-4); KSI-PIY-GIV (CM-8) | findings.json, NPI-OWN-06 and -07: each identity names an active record |
| AC-2(3) Disable Accounts; CM-3 Configuration Change Control | KSI-IAM-AAM (AC-2(3)); KSI-CMT-LMC, KSI-CMT-RVP (CM-3) | orphan-plan.json and its SHA-256; results.json and manifest.json in the apply folder |
| AC-5 Separation of Duties | KSI-IAM-JIT | igo-confirmations.csv; the plan hash approved by a second person |
| AC-6 Least Privilege | KSI-IAM-ELP, KSI-IAM-JIT | Get-NpiRequiredPermission for ownership and orphan-apply |
KSIs are FedRAMP 20x indicators for cloud service providers. The agency lines this evidence up with what its providers report against them; it doesn’t report KSIs itself. Which file supports which indicator is the program’s mapping (design guidance).
Learning objectives (1 minute)
After this module, you’ll be able to:
- state the program’s ownership rule, and which owners the code counts as valid;
- explain D-NP2, the orphan rule the program settled on, and how it differs from Access & Privilege’s own policy;
- run the ownership check and read its findings and its orphan plan;
- show that the write path dry-runs without
-Applyand refuses a plan whose hash nobody approved.
Prerequisites (1 minute)
- Module 1: this module checks Module 1’s register.
- The guide’s record each identity joins to: the owner and sponsor fields.
Unit 1: Owners and sponsors in the guide (8 minutes)
Read the guide’s Step 1f, under Step 1: two human owners, a business approver in the registry, and a sponsor who is a federal employee. Then read Step 6’s Owner change and Orphans, and its table Decommissioning: disable first, then delete.
Unit 2: The check and the plan (14 minutes)
Read the workbook chapter in this order:
- The rule the code holds: Learn’s two-owner advice, the program’s stricter rule (design guidance), D-NP2, and “disable, never delete.”
- The check: NPI-OWN-01 to -09, and the AI sponsor fields recorded for Module 9.
- The orphan rule as a plan: each action, and which ones write.
- The separate write path: the approved hash, the seven-day limit, the allowed bodies, and why a person, not an app, runs it.
Exercise: Lab 1, fixtures, no tenant (22 minutes)
This is the workbook’s Chapter 02, Lab 1, which explains each result. Work in examples/non-person-identity, in PowerShell 7. The estate is fictional. Step 1 makes Chapter 01’s register; the chapter’s step 1 is this module’s step 2, and so on.
Every command in this lab, in order, from examples/non-person-identity. Each step below says what it does and what to look for.
# Step 1: Make Chapter 01's register
./Invoke-NpiInventory.ps1 -Cloud gcc -FixturePath tests/fixtures/estate.json -OutputPath out/inventory
# Step 2: Send Learn's samples to the fake
Invoke-Pester -Path tests/MicrosoftSamples.Tests.ps1 -FullNameFilter '*Chapter 02*' -Output None -PassThru |
Select-Object PassedCount, FailedCount, NotRunCount
# Step 3: List what each task needs
Import-Module ./powershell/NpiOwnership/NpiOwnership.psm1
foreach ($t in 'ownership', 'orphan-apply') { $p = Get-NpiRequiredPermission -Task $t; "$t : " + ($p.GraphPermissions -join ', ') }
# Step 4: Check ownership and build the plan
./Invoke-NpiOwnership.ps1 -Cloud gcc -RegisterRun out/inventory -FixturePath tests/fixtures/estate.json -InputPath tests/fixtures/inputs -OutputPath out/ownership
# Step 5: Read the findings
Get-Content out/ownership/findings.json -Raw | ConvertFrom-Json | Where-Object Severity -eq 'Finding' | Group-Object Rule | Format-Table Name, Count
# Step 6: Read the plan
Get-Content out/ownership/orphan-plan.json -Raw | ConvertFrom-Json | Select-Object -ExpandProperty actions | Format-Table Action, Name, Write
# Step 7: Dry-run the plan with its approved hash
$sha = (Get-FileHash out/ownership/orphan-plan.json -Algorithm SHA256).Hash.ToLower()
./Invoke-NpiOrphanAction.ps1 -Cloud gcc -PlanPath out/ownership/orphan-plan.json -ApprovedSha256 $sha -FixturePath tests/fixtures/estate.json -OutputPath out/orphan-dryrun
# Step 8: Apply it to the fake, then try a hash nobody approved
./Invoke-NpiOrphanAction.ps1 -Cloud gcc -PlanPath out/ownership/orphan-plan.json -ApprovedSha256 $sha -FixturePath tests/fixtures/estate.json -OutputPath out/orphan-apply -Apply
./Invoke-NpiOrphanAction.ps1 -Cloud gcc -PlanPath out/ownership/orphan-plan.json -ApprovedSha256 ('0' * 64) -FixturePath tests/fixtures/estate.json -OutputPath out/orphan-x
# Step 9: Run the tests
Invoke-Pester -Path tests
# After the steps
Remove-Item -Recurse -Force outMake Chapter 01’s register:
./Invoke-NpiInventory.ps1 -Cloud gcc -FixturePath tests/fixtures/estate.json -OutputPath out/inventoryIt prints the three lines of Module 1’s step 3, ending
Written to out/inventory.Send Learn’s samples to the fake:
Invoke-Pester -Path tests/MicrosoftSamples.Tests.ps1 -FullNameFilter '*Chapter 02*' -Output None -PassThru | Select-Object PassedCount, FailedCount, NotRunCountExpect
PassedCount 10,FailedCount 0,NotRunCount 20. The twenty not run are Chapter 01’s.List what each task needs:
Import-Module ./powershell/NpiOwnership/NpiOwnership.psm1 foreach ($t in 'ownership', 'orphan-apply') { $p = Get-NpiRequiredPermission -Task $t; "$t : " + ($p.GraphPermissions -join ', ') }ownership : User.Read.All orphan-apply : Application.Read.All, Application.ReadWrite.All, Directory.Read.All, User.Read.AllThe check reads people only. The write task is separate.
Check ownership and build the plan:
./Invoke-NpiOwnership.ps1 -Cloud gcc -RegisterRun out/inventory -FixturePath tests/fixtures/estate.json -InputPath tests/fixtures/inputs -OutputPath out/ownershipOwnership: 22 identities checked; 7 orphan candidates Findings: 20 Finding, 3 Warn, 1 Info. AI sponsor fields recorded for Chapter 09: 2 Orphan plan: 10 actions, 3 of them writes (1 AddOwner, 2 Disable); nothing sent Plan SHA-256: 1e1799d7181bd1a3dc271c645fff8a60fc741ab1ebc219bb3011cc25953bad3eThe plan’s hash is the same on every run with these inputs.
Read the findings:
Get-Content out/ownership/findings.json -Raw | ConvertFrom-Json | Where-Object Severity -eq 'Finding' | Group-Object Rule | Format-Table Name, CountName Count ---- ----- NPI-OWN-01 2 NPI-OWN-02 4 NPI-OWN-06 6 NPI-OWN-07 3 NPI-OWN-08 5Legacy Reports and Old SAML Connector have no valid owner. Build Pipeline’s second owner is a service principal, and Vendor Portal’s is a guest, so each has only one.
Read the plan:
Get-Content out/ownership/orphan-plan.json -Raw | ConvertFrom-Json | Select-Object -ExpandProperty actions | Format-Table Action, Name, WriteExpect ten actions. Three write:
Disableforsvc-backupand Legacy Reports, andAddOwnerfor HR Connector.svc-reportsis aRetirementCandidate: disabled, and nobody claimed it in 30 days. The Build Pipeline claim named a guest, so it’sNomineeRefused.Dry-run the plan with its approved hash:
$sha = (Get-FileHash out/ownership/orphan-plan.json -Algorithm SHA256).Hash.ToLower() ./Invoke-NpiOrphanAction.ps1 -Cloud gcc -PlanPath out/ownership/orphan-plan.json -ApprovedSha256 $sha -FixturePath tests/fixtures/estate.json -OutputPath out/orphan-dryrunDryRun: 3 WouldSend WouldSend Disable WRITE Disable-ADAccount -Identity ad000000-0000-4000-8000-000000000003 WouldSend Disable PATCH https://graph.microsoft.com/v1.0/servicePrincipals/5b000000-0000-4000-8000-000000000003 WouldSend AddOwner POST https://graph.microsoft.com/v1.0/applications/0a000000-0000-4000-8000-000000000002/owners/$refApply it to the fake, then try a hash nobody approved:
./Invoke-NpiOrphanAction.ps1 -Cloud gcc -PlanPath out/ownership/orphan-plan.json -ApprovedSha256 $sha -FixturePath tests/fixtures/estate.json -OutputPath out/orphan-apply -Apply ./Invoke-NpiOrphanAction.ps1 -Cloud gcc -PlanPath out/ownership/orphan-plan.json -ApprovedSha256 ('0' * 64) -FixturePath tests/fixtures/estate.json -OutputPath out/orphan-xExpect
Apply: 3 Sent, then the refusal:The plan's SHA-256 is 1e1799d7…, not the approved 0000…; refusing to run a plan nobody reviewed.Inout/orphan-apply/results.json, each disable says to recordDisabledOn 2026-10-30and that the claim window ends2026-11-29.Run the tests:
Invoke-Pester -Path testsExpect 513 tests passed, 0 failed.
Delete out when you’re done. It names accounts and owners:
Remove-Item -Recurse -Force outExercise: Lab 2, your test tenant (5 minutes to read)
Not run by the authors. Follow the workbook’s Chapter 02, Lab 2. It has the exact commands.
- You need: a test tenant in the worldwide or GCC cloud and a test Active Directory domain, never production, with fictional people, and Module 1’s Lab 2 run (
out/inventory-test). - What you do: make two test orphans, an app registration with no owners and a test service account; run the check with an app holding only
User.Read.All; confirm both as the identity governance officer; have a second person approve the plan’s hash; then dry-run and apply it as a person with Cloud Application Administrator. - Back-out: step 6 sets the service principal’s
accountEnabledback totrue, enables the test account, deletes both test objects and the test app’s registration, and deletes theoutfolders.
Knowledge check (5 minutes)
- Which owners does the code count as valid?
- Any owner listed on the app
- Users who are members and enabled
- Users and service principals
- Guests with a sponsor
- Under D-NP2, as the plan states it, what happens to a confirmed orphan?
- It’s deleted after 30 days
- It’s disabled once the identity governance officer confirms it, then there are 30 days to claim it or retire it
- Nothing until day 31
- It’s moved to a holding OU and kept forever
- How does the code disable an orphaned app registration’s access?
- It deletes the app registration
- It deactivates the app registration on the beta endpoint
- It sets
accountEnabledto false on the tenant’s service principal, on v1.0 - It removes every owner
- You run
Invoke-NpiOrphanAction.ps1with an-ApprovedSha256that isn’t the plan’s hash. What happens?- It sends the writes and logs a warning
- It refuses to run a plan nobody reviewed
- It recomputes the plan
- It sends only the disables
- Why is the write path a person’s delegated session, not an app with a standing permission?
Application.ReadWrite.Allcan let an app act as other entities, and disabling is never automatic- Apps can’t call PATCH
- Delegated sessions don’t need a role
- The fake requires it
- b. Only enabled members count; guests and service principals raise warnings. Taught in The rule the code holds and The check.
- b. The program settled on D-NP2’s order, the stricter: disable first, then the 30-day claim window. The guide and the code follow it; Access & Privilege, Chapter 06 runs the other way round. Taught in The rule the code holds.
- c. Learn’s deactivation procedure is on beta, so the code uses the v1.0 switch. Taught in The rule the code holds.
- b. Taught in The separate write path and shown in Lab 1, step 8.
- a. Taught in The separate write path.
Summary (2 minutes)
You checked 22 identities for valid owners and active records, built an orphan plan of ten actions with three writes, dry-ran it, applied it to the fake, and saw the write path refuse a hash nobody approved.
The evidence an ISSO keeps from this module: ownership.json, findings.json and orphan-plan.json with its approved SHA-256; igo-confirmations.csv; and the dry-run and apply folders with their results.json and manifest.json, dated. Before shipping, work through the chapter’s Checklist.