In this module
MSA1.13 Guided Walkthrough — The Identity Layer as a System
You've completed 11 content subs and built the lab baseline. You've learned about tenant boundaries, identity types, attack surfaces, hybrid architecture, legacy components, AUs, lifecycle, stale identities, naming, groups, and the assessment. Each sub produced specific findings and specific queries. What you may not yet see clearly is how all of these connect — how a gap in one area (lifecycle automation) directly causes a finding in another (stale identity accumulation), which creates a constraint in a third (access reviews can't run without group owners). This walkthrough teaches the system view by having you run connected queries against your lab tenant. Each exercise bridges two or more subs, showing you the dependency chain that makes identity architecture a system.
Prerequisites: Your lab tenant from MSA1.12, with all persona accounts, groups, AUs, and intentional gaps created. Connect to Graph before starting:
Connect-MgGraph -Scopes "User.Read.All","Group.Read.All","AuditLog.Read.All",
"Directory.Read.All","AdministrativeUnit.Read.All","Application.Read.All",
"Policy.Read.All"
$domain = (Get-MgOrganization).VerifiedDomains |
Where-Object { $_.IsDefault } |
Select-Object -ExpandProperty NameExercise 1 — Census to group coverage: are all identities in groups?
This exercise connects MSA1.2 (census) to MSA1.10 (group architecture). The census tells you what exists. The group architecture tells you how access is mediated. But are all identities actually in the right groups? If someone exists in the census but isn't in any department group, they're invisible to every CA policy, lifecycle workflow, and access review that targets department groups.
# Get all member users
$members = Get-MgUser -All -Property DisplayName, Department, UserType |
Where-Object { $_.UserType -eq 'Member' -and $_.DisplayName -notmatch 'Break Glass' }
Write-Host "Member users (excl. break-glass): $($members.Count)"
Write-Host ""
# For each member, check if they're in their department's SG group
$ungrouped = foreach ($user in $members) {
$dept = $user.Department
if (-not $dept) {
[PSCustomObject]@{ Name=$user.DisplayName; Issue="No Department attribute"; Group="N/A" }
continue
}
$expectedGroup = "SG-$dept-All"
$userGroups = Get-MgUserMemberOf -UserId $user.Id -All |
Where-Object { $_.AdditionalProperties['displayName'] -eq $expectedGroup }
if (-not $userGroups) {
[PSCustomObject]@{ Name=$user.DisplayName; Issue="Not in department group"; Group=$expectedGroup }
}
}
if ($ungrouped) {
Write-Host "⚠ IDENTITY-TO-GROUP GAPS:"
$ungrouped | Format-Table -AutoSize
} else {
Write-Host "✓ All members are in their department groups"
}What this teaches: The census alone doesn't tell you whether access controls cover everyone. A user with Department: Engineering who isn't in SG-Engineering-All is invisible to every Engineering-targeted policy. The census (MSA1.2) and the group architecture (MSA1.10) must be cross-referenced to find coverage gaps.
Entra Admin Center
Identity → Users → select any persona → Groups
Verify the user appears in their expected department group. If they don't, the lab population script in MSA1.12 may have missed them — or their Department attribute may be empty.
Exercise 2 — The mover problem in your tenant: Priya's accumulated access
This exercise connects MSA1.7 (lifecycle) to MSA1.10 (groups) to MSA1.8 (stale identities). MSA1.7 taught that the mover stage causes access accumulation. MSA1.12's lab deliberately gave Priya both SOC and Engineering memberships. Now you'll see what this looks like from a governance perspective — the exact query an access reviewer would run.
$priya = Get-MgUser -UserId "p.sharma@$domain"
$priyaGroups = Get-MgUserMemberOf -UserId $priya.Id -All |
Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group' }
Write-Host "SOC Analyst 2 — $($priya.Department), $($priya.JobTitle)"
Write-Host "Total group memberships: $($priyaGroups.Count)"
Write-Host ""
# Classify each group as current or stale based on Department
foreach ($group in $priyaGroups) {
$name = $group.AdditionalProperties['displayName']
# Priya is in Security. Engineering/CAD/PLM/Safety groups are stale.
$stale = $name -match 'Engineering|CAD|PLM|Safety'
$verdict = if ($stale) { "⚠ STALE — previous role" } else { "✓ Current" }
Write-Host " $verdict : $name"
}
Write-Host ""
$staleCount = ($priyaGroups | Where-Object {
$_.AdditionalProperties['displayName'] -match 'Engineering|CAD|PLM|Safety'
}).Count
Write-Host "Current: $($priyaGroups.Count - $staleCount) groups"
Write-Host "Stale: $staleCount groups (from Engineering role + Safety project)"
Write-Host ""
Write-Host "This is the mover problem. Priya changed departments 3 months ago."
Write-Host "Nobody removed her old access. An access review (MSA12) would catch this."
Write-Host "A mover workflow (MSA1.7) would have prevented it."What this teaches: The lifecycle gap (MSA1.7) directly produces the stale access that MSA1.8 measures. And the fix requires both a reactive mechanism (access reviews — MSA12) and a preventive mechanism (mover workflows — MSA1.7). The walkthrough connects three subs through one query.
Exercise 3 — Naming convention compliance: can you query your tenant programmatically?
This exercise connects MSA1.9 (naming) to MSA1.10 (groups) to MSA3 (future CA policy design). If your groups don't follow a naming convention, you can't answer governance questions programmatically. Run this query and see which questions are answerable and which aren't.
$groups = Get-MgGroup -All -Property DisplayName
# Question 1: How many department groups exist?
$deptGroups = $groups | Where-Object { $_.DisplayName -match '^SG-.+-All$' }
Write-Host "Q1: Department groups (SG-*-All): $($deptGroups.Count)"
$deptGroups | ForEach-Object { Write-Host " $($_.DisplayName)" }
Write-Host ""
# Question 2: How many CA exclusion groups exist?
$caExclude = $groups | Where-Object { $_.DisplayName -match '^SG-CA-Exclude' }
Write-Host "Q2: CA exclusion groups (SG-CA-Exclude-*): $($caExclude.Count)"
$caExclude | ForEach-Object { Write-Host " $($_.DisplayName)" }
Write-Host ""
# Question 3: How many groups DON'T follow the naming convention?
$nonCompliant = $groups | Where-Object {
$_.DisplayName -notmatch '^(SG-|M365-|DG-|DL-|SG-CA-)'
}
Write-Host "Q3: Non-compliant group names: $($nonCompliant.Count)"
$nonCompliant | ForEach-Object { Write-Host " ❌ $($_.DisplayName)" }
Write-Host ""
Write-Host "The non-compliant groups are the intentional gaps from MSA1.12."
Write-Host "In the organization's production tenant, 127 groups (59%) would be non-compliant."
Write-Host "Each non-compliant group is invisible to programmatic governance."What this teaches: The naming convention (MSA1.9) isn't about aesthetics — it's about whether your governance is programmable. Every non-compliant group is a group you can't find, can't audit, and can't automate lifecycle management for.
Entra Admin Center
Identity → Groups → All groups → sort by Name
Scroll through the list. Notice how the SG- prefixed groups are visually grouped together while "old contractors group" and "Project Safety" stand out as anomalies. This is the visual equivalent of the programmatic query — consistent naming makes anomalies immediately visible.
Exercise 4 — AU-to-group alignment: do delegation boundaries match CA targeting?
This exercise connects MSA1.6 (AUs) to MSA1.10 (groups). AUs scope admin delegation. Groups scope CA policy targeting. If the same population (Manchester staff) needs both, the membership must match. This query checks alignment.
# Get Manchester AU members
$manchesterAU = Get-MgDirectoryAdministrativeUnit -All |
Where-Object { $_.DisplayName -eq "NE-Site-Manchester" }
if ($manchesterAU) {
$auMembers = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $manchesterAU.Id -All
# Get Manchester group members
$manchesterGroup = Get-MgGroup -Filter "displayName eq 'SG-Manchester-Staff'"
$groupMembers = Get-MgGroupMember -GroupId $manchesterGroup.Id -All
Write-Host "AU members (NE-Site-Manchester): $($auMembers.Count)"
Write-Host "Group members (SG-Manchester-Staff): $($groupMembers.Count)"
Write-Host ""
# Find mismatches
$auIds = $auMembers.Id
$groupIds = $groupMembers.Id
$inAUonly = $auIds | Where-Object { $_ -notin $groupIds }
$inGroupOnly = $groupIds | Where-Object { $_ -notin $auIds }
if ($inAUonly.Count -eq 0 -and $inGroupOnly.Count -eq 0) {
Write-Host "✓ Perfect alignment — AU and group have identical membership"
Write-Host " Manchester Helpdesk Admin scope = Manchester CA policy scope"
} else {
Write-Host "⚠ MISALIGNMENT:"
Write-Host " In AU but NOT in group: $($inAUonly.Count) users"
Write-Host " In group but NOT in AU: $($inGroupOnly.Count) users"
Write-Host ""
Write-Host " Users in the AU but not the group: admin-delegated but NOT"
Write-Host " covered by Manchester-specific CA policies."
Write-Host " Users in the group but not the AU: covered by CA but NOT"
Write-Host " manageable by the Manchester Helpdesk Administrator."
}
} else {
Write-Host "NE-Site-Manchester AU not found — create it in the lab first"
}What this teaches: AUs and groups are complementary — not interchangeable (MSA1.6). If you design site-based delegation (AU) and site-based CA policies (group) separately, the populations can drift apart. The architectural fix: use the same membership source for both. If the AU is assigned, keep the group assigned and synchronize membership through automation. If both were dynamic, both would use the same rule — guaranteeing alignment.
Exercise 5 — Group health: which groups are governance-ready?
This exercise connects MSA1.10 (groups) to MSA12 (future access reviews). A group is governance-ready if it has members, an owner, a description, and follows the naming convention. Groups that lack any of these can't be effectively reviewed.
$groups = Get-MgGroup -All -Property DisplayName, Description
$results = foreach ($group in $groups) {
$members = (Get-MgGroupMember -GroupId $group.Id -All -ErrorAction SilentlyContinue)
$owners = (Get-MgGroupOwner -GroupId $group.Id -All -ErrorAction SilentlyContinue)
$issues = @()
if ($members.Count -eq 0) { $issues += "Empty" }
if ($owners.Count -eq 0) { $issues += "No owner" }
if (-not $group.Description) { $issues += "No description" }
if ($group.DisplayName -notmatch '^(SG-|M365-|DG-|DL-)') { $issues += "Bad name" }
[PSCustomObject]@{
Name = $group.DisplayName
Members = $members.Count
Owners = $owners.Count
HasDesc = [bool]$group.Description
Issues = if ($issues) { $issues -join ", " } else { "None — governance-ready" }
Ready = $issues.Count -eq 0
}
}
$ready = ($results | Where-Object { $_.Ready }).Count
$notReady = ($results | Where-Object { -not $_.Ready }).Count
Write-Host "=== GROUP GOVERNANCE READINESS ==="
Write-Host "Governance-ready: $ready ($([math]::Round($ready/$results.Count*100))%)"
Write-Host "Not ready: $notReady ($([math]::Round($notReady/$results.Count*100))%)"
Write-Host ""
Write-Host "Groups NOT governance-ready:"
$results | Where-Object { -not $_.Ready } |
Select-Object Name, Members, Owners, Issues | Format-Table -AutoSizeWhat this teaches: MSA12's access reviews require groups to have owners (someone to review), members (someone to review about), and descriptions (context for the reviewer). The group sprawl findings from MSA1.10 — 31 empty, 67 ownerless — aren't just messy administration. They're governance blockers. Until these groups are cleaned up or deleted, access reviews can't cover them.
Exercise 6 — The full identity health dashboard
This exercise produces the complete identity health view by running the key diagnostic queries from MSA1.2, MSA1.8, MSA1.9, and MSA1.10 in sequence. Think of this as the script you'd run monthly to track improvement over time.
Write-Host "==========================================="
Write-Host " IDENTITY ARCHITECTURE HEALTH DASHBOARD"
Write-Host "==========================================="
Write-Host ""
# 1. Census (MSA1.2)
$allUsers = Get-MgUser -All -Property UserType, AccountEnabled, Department
$members = $allUsers | Where-Object { $_.UserType -eq 'Member' }
$guests = $allUsers | Where-Object { $_.UserType -eq 'Guest' }
Write-Host "[CENSUS]"
Write-Host " Members: $($members.Count)"
Write-Host " Guests: $($guests.Count)"
Write-Host " Total users: $($allUsers.Count)"
Write-Host ""
# 2. Attribute quality (MSA1.6/1.7)
$withDept = ($members | Where-Object { $_.Department }).Count
$deptPct = if ($members.Count -gt 0) { [math]::Round($withDept/$members.Count*100,1) } else { 0 }
Write-Host "[ATTRIBUTE QUALITY]"
Write-Host " Department coverage: $withDept / $($members.Count) ($deptPct%)"
Write-Host " Target: 100% (required for dynamic groups and lifecycle workflows)"
Write-Host ""
# 3. Group health (MSA1.10)
$allGroups = Get-MgGroup -All -Property DisplayName
$compliantNames = ($allGroups | Where-Object {
$_.DisplayName -match '^(SG-|M365-|DG-|DL-)'
}).Count
$namePct = [math]::Round($compliantNames/$allGroups.Count*100)
Write-Host "[GROUP HEALTH]"
Write-Host " Total groups: $($allGroups.Count)"
Write-Host " Naming compliance: $compliantNames / $($allGroups.Count) ($namePct%)"
Write-Host " Target: 95%+ naming compliance"
Write-Host ""
# 4. AU coverage (MSA1.6)
$aus = Get-MgDirectoryAdministrativeUnit -All
Write-Host "[ADMINISTRATIVE UNITS]"
Write-Host " Total AUs: $($aus.Count)"
$aus | ForEach-Object {
$memberCount = (Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $_.Id -All).Count
$restricted = if ($_.IsMemberManagementRestricted) { " [RESTRICTED]" } else { "" }
Write-Host " $($_.DisplayName): $memberCount members$restricted"
}
Write-Host ""
# 5. Intentional gaps remaining (MSA1.12)
Write-Host "[GAPS TO FIX]"
$mfaExclude = Get-MgGroup -Filter "displayName eq 'SG-CA-Exclude-MFA'" -ErrorAction SilentlyContinue
if ($mfaExclude) {
$mfaMembers = (Get-MgGroupMember -GroupId $mfaExclude.Id -All).Count
Write-Host " ⚠ MFA exclusion group exists with $mfaMembers members (fix in MSA2)"
} else {
Write-Host " ✓ MFA exclusion group removed"
}
$badGroups = $allGroups | Where-Object { $_.DisplayName -in @("old contractors group","Project Safety") }
Write-Host " ⚠ Non-compliant groups remaining: $($badGroups.Count) (fix in MSA1.10)"
Write-Host ""
Write-Host "==========================================="
Write-Host " Run this dashboard monthly to track"
Write-Host " improvement as you progress through MSA2+"
Write-Host "==========================================="What this teaches: Identity architecture health is measurable. Census size, attribute quality, naming compliance, AU coverage, and known gaps are all queryable metrics. Running this dashboard monthly shows whether the architecture is improving (naming compliance increases, gaps decrease, attribute coverage rises) or degrading (new non-compliant groups appear, gaps persist). The dashboard connects every MSA1 sub into a single operational view.
Entra Admin Center
Identity → Overview → Recommendations
Microsoft's built-in recommendations surface some of these same findings (inactive users, missing MFA registration, stale apps). Compare the recommendations to your dashboard output — the dashboard queries are more specific because they're designed for your architecture, not for a generic tenant.
Exercise 7 — The dependency chain: what breaks if you skip a step?
This final exercise is conceptual. No query to run — instead, trace the dependency chain that the previous 6 exercises revealed. Answer each question by referencing what you saw in the exercises above.
Question 1: If NE never connects the HR system (MSA1.7 Tier 4, item 16), what can't happen?
The employeeHireDate and employeeLeaveDateTime attributes stay at 0%. Time-based Lifecycle Workflow triggers can't fire. Joiner workflows can't pre-provision accounts before Day 1. Leaver workflows can't automatically disable accounts on the leave date. The mover problem persists — Priya's accumulated access (Exercise 2) is reproduced for every role change. Stale identity counts (MSA1.8) don't decrease because the lifecycle that would prevent accumulation doesn't exist. MSA12's governance architecture has no automated enforcement mechanism — it's reduced to periodic manual reviews.
Question 2: If NE never assigns owners to the 67 ownerless groups (MSA1.10), what can't happen?
Access reviews (MSA12) can't run for those groups because there's no reviewer. Exercise 5's governance readiness check showed which groups aren't review-ready. Without reviews, stale memberships in those groups persist indefinitely. Priya's accumulated access (Exercise 2) is discovered — eventually — but only for groups that have owners. The 67 ownerless groups are governance blind spots.
Question 3: If NE implements CA policies (MSA3) without first establishing the naming convention (MSA1.9) and group architecture (MSA1.10), what goes wrong?
The CA policies target ad-hoc groups with inconsistent names. Exercise 3 showed that non-compliant groups can't be found programmatically. When Marcus needs to audit "which CA policies target Engineering?" he can't query it — he has to open each policy. When the audit reveals gaps, he can't determine whether "Engineering - All Staff" and "SG-Engineering-All" are the same group or different. The CA framework becomes unmanageable at scale.
The security architect jumps straight to MSA3 (Conditional Access) because CA policies are the visible security control. They create 15 CA policies targeting ad-hoc groups with inconsistent names. Six months later, nobody can audit the policies because the group names are incomprehensible. New employees aren't in the right groups because there's no lifecycle automation. Guests accumulate because there's no review process. The CA framework looks impressive in the policy list but has coverage gaps nobody can find because the identity layer underneath was never architected. Module 1 exists to prevent this — the identity layer is the foundation that determines whether CA (MSA3), PIM (MSA4), data protection (MSA5), and everything else works at scale or degrades into unmanageable ad-hoc configuration.
Before moving on, verify your results: run Exercise 1 against your lab tenant and check every member user appears in their department's SG group. Run the health dashboard from Exercise 6 and note your naming compliance and attribute quality percentages — these are your Module 1 baseline.
Reusable script — save the Exercise 6 health dashboard script to your architecture package at 01-identity/scripts/identity-health-dashboard.ps1. Run it monthly against your production tenant to track improvement as you implement the MSA1 remediation roadmap. The dashboard produces 5 metrics: census size, attribute quality, group health, AU coverage, and intentional gap status. As you progress through MSA2–MSA14, the gap count should decrease toward zero.
You're reading the free modules of m365-security-architecture
The full course continues with advanced topics, production detection rules, worked investigation scenarios, and deployable artifacts.