Lab Metadata
Core technologies
Scenario and Description
Northgate Financial is a mid-sized UK financial services firm with roughly 4,800 staff across retail banking, operations and a small cyber security function. An internal audit and a follow-up penetration test have both flagged the same theme: identity is the weak edge. Some staff still complete multi-factor authentication over SMS, a handful of administrators have no strong second factor at all, several long-departed contractors retain active accounts, and nobody can produce a single authoritative view of who holds privileged roles.
You have been brought in as the identity security specialist to lead the remediation programme. Before you change a single policy you need a baseline: a measured, evidence-backed picture of the current risk. Auditors will ask what the position was before and after. Change controls will ask you to justify each hardening step. Executives will want a number that moves. All of that starts here, with a read-only assessment run through the Microsoft Graph.
In this lab you build the reproducible lab environment used by every later lab in the track, seed it with the Northgate personas, then run a baseline script that reports MFA and self-service password reset registration, privileged role membership, dormant accounts, guest accounts and SMS-reliant users. You finish with CSV evidence and a printed risk summary you could hand to a programme board.
Prerequisites
Prior labs
None. This is the foundation lab. Every later lab in IB-ENTRA-SEC assumes the tenant, module and personas built here.
Knowledge assumed
Zero prior Entra or PowerShell experience is assumed. Where a term appears for the first time, such as "tenant", "scope" or "directory role", it is defined before it is used.
System requirements
| Item | Minimum | Notes |
|---|---|---|
| Operating system | Windows 10/11, macOS 13+, or Ubuntu 22.04 | The Graph SDK is cross-platform |
| PowerShell | 7.4 or later | Windows PowerShell 5.1 works but 7.x is the supported baseline for this track |
| Memory / disk | 4 GB RAM, 2 GB free | The Graph SDK module set is around 700 MB |
| Network | Outbound HTTPS (443) | Reaches graph.microsoft.com and login.microsoftonline.com |
| A test tenant | Entra ID tenant with a P2 trial | See the environment options below. Never run this against a production tenant |
Choosing your lab tenant (verified July 2026)
The free tenant landscape changed, so pick the path that fits you. All later labs need Entra ID P1 or P2 features (Conditional Access needs P1, Privileged Identity Management and Identity Protection need P2, and the signInActivity data used in this lab needs P1). The cleanest way to get all of that at no cost is an Entra ID P2 trial on a fresh Azure tenant.
| Path | What you get | Best for |
|---|---|---|
| A. Microsoft 365 Developer Program (E5 sandbox) | E5 with 25 licences, pre-seeded users, includes Entra ID P2. Renews for the life of your Visual Studio subscription. | Anyone holding a Visual Studio Professional or Enterprise standard subscription. Since 2024 this is effectively the entry requirement, individual sign-ups without it are usually refused. |
| B. Azure free account + Entra ID P2 trial (recommended if you lack path A) | Free Azure tenant, then a 31-day Entra ID P2 trial for up to 100 licences. Covers Conditional Access, PIM, Identity Protection, access reviews and sign-in activity. | Most independent learners. Needs a card for identity verification and a work or school style account in the tenant to activate cleanly. |
| C. Azure free account only (Entra free tier) | Users, groups, app registrations, Graph access. No Conditional Access, PIM or sign-in activity. | Running only this lab's user and role sections. You will hit licence walls from Lab 02 onward. |
azure.microsoft.com/free. Sign in to the Microsoft Entra admin center at entra.microsoft.com, open Billing > Licenses > All products, choose Try / Buy, select Microsoft Entra ID P2 and activate the free trial. If it prompts you to complete a "sold-to" address and payment method on the billing account first, do that, then reload the Licenses blade. Finally assign the P2 licence to your admin user under Licenses > All products > Microsoft Entra ID P2 > Assign.
Required tools with versions
Install PowerShell 7 and verify
# Windows (winget). PowerShell 7 installs alongside Windows PowerShell 5.1. winget install --id Microsoft.PowerShell --source winget # macOS (Homebrew) brew install --cask powershell # Ubuntu 22.04 sudo apt-get update && sudo apt-get install -y wget apt-transport-https software-properties-common wget -q "https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb" sudo dpkg -i packages-microsoft-prod.deb && sudo apt-get update && sudo apt-get install -y powershell # Verify: open a new PowerShell 7 session (command name is: pwsh) and run $PSVersionTable.PSVersion # Expected: Major = 7, Minor = 4 or higher
Install the Microsoft Graph PowerShell SDK and verify
# Run inside pwsh (PowerShell 7). CurrentUser scope avoids needing admin rights. Install-Module Microsoft.Graph -Scope CurrentUser -Repository PSGallery -Force # The meta-module pulls in sub-modules (Users, Reports, Identity.DirectoryManagement, etc.) # Verify the module and a key command are present: Get-Module Microsoft.Graph -ListAvailable | Select-Object Name,Version | Sort-Object Version -Descending | Select-Object -First 1 Get-Command Connect-MgGraph -Module Microsoft.Graph.Authentication # Expected: a version (2.x or later) and the Connect-MgGraph command resolves.
Mg (for example Get-MgUser). Instead of clicking through the portal, you query and change identity objects programmatically, which is exactly what "automating administration and reporting through PowerShell" means in the role you are preparing for.
Real-World Problem Statement
An identity hardening programme that starts by changing controls, rather than by measuring, is one that cannot prove it worked and cannot prioritise. You need to answer four questions with evidence before you touch a policy: how many accounts can actually perform strong MFA, who holds privileged roles, which accounts are dormant, and who still depends on weak factors such as SMS.
| Dimension | Why the baseline matters |
|---|---|
| Risk | Dormant enabled accounts and SMS-only MFA are two of the most commonly exploited footholds. You cannot reduce what you have not counted. |
| Compliance | Audit and frameworks such as ISO 27001 and Cyber Essentials expect evidence of access reviews and MFA coverage. A dated CSV baseline is that evidence. |
| Productivity | A repeatable script replaces days of manual portal exports and can be re-run to show progress week over week to the programme board. |
| Security posture | The baseline becomes the denominator for every later metric: "MFA-capable rose from 61 percent to 100 percent", "privileged role holders fell from 14 to 6". |
Concrete scenario: Northgate's CISO has committed to the board that within one quarter, every account will be MFA-capable, SMS will be retired, and privileged access will be time-bound. Your first deliverable, due this week, is the "as-is" baseline that those commitments are measured against.
Skills Mapped to Production Solutions
| Skill learned in this lab | Real-world enterprise application |
|---|---|
| Connecting to Graph with scoped delegated permissions | Least-privilege operational access for identity engineers, so read tasks never run with write rights |
| Reading the authentication methods registration report | Measuring MFA and SSPR coverage across the estate for board reporting and audit evidence |
| Enumerating directory role membership | Privileged access inventories and the input to a least-privilege and PIM programme |
| Querying sign-in activity to find dormancy | Joiner-mover-leaver hygiene and automated deprovisioning candidates |
| Exporting structured evidence to CSV | Repeatable, dated compliance artefacts and before-and-after remediation metrics |
Architecture Overview
The lab has three planes: your analyst workstation running PowerShell 7 and the Graph SDK, the Microsoft Graph API as the control surface, and the Entra ID tenant holding the identity objects you assess. Nothing you do in this lab writes to the tenant except the one-off persona seeding step, which is clearly isolated.
Component breakdown
| Component | Purpose | Transport | Key configuration |
|---|---|---|---|
| PowerShell 7 + Graph SDK | Runs the assessment cmdlets | Local process | Module installed at CurrentUser scope |
| Microsoft Graph | Authenticated control surface for Entra | HTTPS 443 | Delegated OAuth scopes, consented at sign-in |
| Entra ID tenant | Holds users, roles, registration and sign-in data | Managed by Microsoft | P2 trial licence assigned to enable premium data |
| CSV evidence | Dated, portable audit artefact | Local disk | One folder per run, timestamped |
Data flow
- Authenticate:
Connect-MgGraphopens a browser sign-in and requests only the scopes you name, so consent is explicit and minimal. - Query: each cmdlet calls a Graph v1.0 endpoint. The registration report, role membership and sign-in activity are separate endpoints, combined client-side.
- Assess: PowerShell filters the returned objects into risk categories (not MFA-capable, dormant, privileged, SMS-reliant).
- Evidence: results are written to timestamped CSVs and printed as a summary you can screenshot for the board.
Step-by-Step Implementation
Phase A - Connect with least privilege
1Sign in to Graph with read-only scopes
Purpose: establish an authenticated session that can read identity data and nothing more.
Context: a "scope" is a named permission such as User.Read.All. Requesting only read scopes means even a mistake in your script cannot modify the tenant.
Connect and confirm the session
# Read-only scopes for the whole baseline. # User.Read.All -> read user objects # AuditLog.Read.All -> read registration report and signInActivity # Directory.Read.All -> read directory roles and members # RoleManagement.Read.Directory -> read role assignments $readScopes = @( 'User.Read.All', 'AuditLog.Read.All', 'Directory.Read.All', 'RoleManagement.Read.Directory' ) Connect-MgGraph -Scopes $readScopes -NoWelcome # Confirm who you are and which scopes were granted: $ctx = Get-MgContext $ctx | Select-Object Account,TenantId,@{n='Scopes';e={($_.Scopes -join ', ')}}
Get-MgContext returns your account, the tenant ID, and a scope list that includes the four read scopes above. If the browser consent prompt lists write permissions, you named the wrong scopes, disconnect with Disconnect-MgGraph and reconnect.
What just happened? You exchanged an interactive sign-in for a short-lived access token carrying exactly the four permissions you asked for. Every later cmdlet rides on that token. Because none of the scopes end in .ReadWrite.All, the tenant is safe from accidental change during assessment.
Phase B - Seed the Northgate personas
2Create test users with a deliberately mixed risk profile
Purpose: give the baseline something realistic to find: a normal user, an administrator, a dormant leaver and a guest.
Context: we reuse the Northgate personas from the wider Identity Bytes labs so the narrative stays consistent: asmith (finance), jpatel (IT operations, will be made an admin), lokafor (cyber security).
Connect with write scopes and create the users
# Elevate to write scopes ONLY for this seeding step. Connect-MgGraph -Scopes 'User.ReadWrite.All','Directory.ReadWrite.All' -NoWelcome # Resolve the tenant's default domain (for example contoso.onmicrosoft.com) $domain = (Get-MgOrganization).VerifiedDomains | Where-Object { $_.IsDefault } | Select-Object -ExpandProperty Name Write-Host "Default domain: $domain" # Persona definitions. Passwords are temporary and must be changed at first sign-in. $personas = @( @{ First='Aisha'; Last='Smith'; Alias='asmith'; Dept='Finance' }, @{ First='Jay'; Last='Patel'; Alias='jpatel'; Dept='IT Operations' }, @{ First='Lola'; Last='Okafor'; Alias='lokafor'; Dept='Cyber Security' }, @{ First='Mark'; Last='Reeves'; Alias='mreeves'; Dept='Contractor (left)' } ) foreach ($p in $personas) { $upn = "$($p.Alias)@$domain" $pwProfile = @{ Password = 'N0rthgate!' + (Get-Random -Minimum 1000 -Maximum 9999) ForceChangePasswordNextSignIn = $true } New-MgUser -DisplayName "$($p.First) $($p.Last)" ` -UserPrincipalName $upn ` -MailNickname $p.Alias ` -AccountEnabled ` -PasswordProfile $pwProfile ` -UsageLocation 'GB' ` -Department $p.Dept | Out-Null Write-Host "Created $upn" }
Make jpatel a privileged admin (so the baseline has a role holder to find)
# Activate the 'User Administrator' directory role if it is not already active, # then add jpatel. Role template IDs are fixed GUIDs published by Microsoft. # fe930be7-5e62-47db-91af-98c3a49a38b1 = User Administrator $roleTemplateId = 'fe930be7-5e62-47db-91af-98c3a49a38b1' $role = Get-MgDirectoryRole -All | Where-Object { $_.RoleTemplateId -eq $roleTemplateId } if (-not $role) { $role = New-MgDirectoryRole -RoleTemplateId $roleTemplateId } $jpatel = Get-MgUser -Filter "userPrincipalName eq 'jpatel@$domain'" New-MgDirectoryRoleMemberByRef -DirectoryRoleId $role.Id -BodyParameter @{ '@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$($jpatel.Id)" } Write-Host "jpatel added to $($role.DisplayName)"
Get-MgUser -Filter "department eq 'Finance'" | Select DisplayName,UserPrincipalName. You should see Aisha Smith. If New-MgUser returns an authorization error, your session still holds only read scopes, reconnect with the write scopes shown above.
What just happened? You created four users whose combined shape mirrors a real tenant: an ordinary user, an admin, a security analyst, and a contractor who will read as dormant because it has never signed in. The baseline in Phase C now has genuine findings to surface rather than an empty tenant.
Phase C - Run the identity risk baseline
3Read the MFA and SSPR registration report
Purpose: measure how many accounts can perform strong MFA and how many rely on SMS.
Context: the authentication methods registration report is the authoritative source for who is MFA-capable. It replaces the old and unreliable habit of inferring MFA from per-user settings.
Pull registration details and classify SMS reliance
# Reconnect read-only if you elevated in Phase B. Connect-MgGraph -Scopes $readScopes -NoWelcome # One row per user: MFA capability, SSPR status, and the methods they registered. $reg = Get-MgReportAuthenticationMethodUserRegistrationDetail -All $mfaCapable = $reg | Where-Object { $_.IsMfaCapable } $mfaNotCapable = $reg | Where-Object { -not $_.IsMfaCapable } $ssprReg = $reg | Where-Object { $_.IsSsprRegistered } # SMS-reliant = has mobile phone method but no Authenticator push and no FIDO2 key. # These are your migration targets for Lab 02. $smsReliant = $reg | Where-Object { ($_.MethodsRegistered -contains 'mobilePhone') -and -not ($_.MethodsRegistered -contains 'microsoftAuthenticatorPush') -and -not ($_.MethodsRegistered -contains 'fido2SecurityKey') }
IsMfaCapable = false because they have not registered a method yet. That is correct and useful: it is exactly the "no MFA configured" population Lab 06 will automate the disabling of.
4Inventory privileged role holders
Purpose: produce the single authoritative list of who holds directory roles.
Context: a "directory role" grants administrative power in Entra (for example Global Administrator, User Administrator). Counting and naming these holders is the first move of any least-privilege programme.
Enumerate active directory roles and their members
$privReport = foreach ($role in (Get-MgDirectoryRole -All)) {
$members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All
foreach ($m in $members) {
[pscustomobject]@{
Role = $role.DisplayName
MemberType = ($m.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.','')
DisplayName = $m.AdditionalProperties['displayName']
Upn = $m.AdditionalProperties['userPrincipalName']
}
}
}
$privReport | Sort-Object Role | Format-Table -AutoSize
5Find dormant accounts from sign-in activity
Purpose: identify enabled accounts that have not signed in recently, the classic leaver and stale-service-account risk.
Context: signInActivity is a premium property, it requires Entra ID P1 and the AuditLog.Read.All scope. It must be requested explicitly with -Property.
Query last sign-in and flag accounts idle beyond 90 days
$props = 'id','displayName','userPrincipalName','accountEnabled',
'createdDateTime','userType','signInActivity'
$allUsers = Get-MgUser -All -Property $props
$cutoff = (Get-Date).AddDays(-90)
$dormant = $allUsers | Where-Object {
$_.AccountEnabled -and
(
$null -eq $_.SignInActivity -or
$null -eq $_.SignInActivity.LastSignInDateTime -or
$_.SignInActivity.LastSignInDateTime -lt $cutoff
)
}
$guests = $allUsers | Where-Object { $_.UserType -eq 'Guest' }
$dormant |
Select-Object DisplayName,UserPrincipalName,
@{n='LastSignIn';e={$_.SignInActivity.LastSignInDateTime}},
@{n='AgeDays';e={ if($_.SignInActivity.LastSignInDateTime){[int]((Get-Date)-$_.SignInActivity.LastSignInDateTime).TotalDays} else {'never'} }} |
Sort-Object AgeDays -Descending |
Format-Table -AutoSize
$cutoff and add a created-date guard for your own estate.
6Assemble the baseline into evidence
Purpose: combine every finding into timestamped CSVs and a printed risk summary suitable for a programme board.
Full script: Invoke-NorthgateIdentityBaseline.ps1
#requires -Version 7.0 # Northgate Financial - read-only identity risk baseline. # Produces CSV evidence and a console summary. Changes nothing in the tenant. $readScopes = @('User.Read.All','AuditLog.Read.All', 'Directory.Read.All','RoleManagement.Read.Directory') Connect-MgGraph -Scopes $readScopes -NoWelcome $stamp = Get-Date -Format 'yyyyMMdd-HHmm' $outDir = Join-Path (Get-Location) "NorthgateBaseline-$stamp" New-Item -ItemType Directory -Path $outDir -Force | Out-Null # --- Registration (MFA / SSPR) --- $reg = Get-MgReportAuthenticationMethodUserRegistrationDetail -All $mfaCapable = $reg | Where-Object { $_.IsMfaCapable } $smsReliant = $reg | Where-Object { ($_.MethodsRegistered -contains 'mobilePhone') -and -not ($_.MethodsRegistered -contains 'microsoftAuthenticatorPush') -and -not ($_.MethodsRegistered -contains 'fido2SecurityKey') } $reg | Select-Object UserPrincipalName,IsAdmin,IsMfaCapable,IsMfaRegistered, IsSsprRegistered,@{n='Methods';e={$_.MethodsRegistered -join ';'}} | Export-Csv (Join-Path $outDir 'registration.csv') -NoTypeInformation # --- Privileged roles --- $priv = foreach ($role in (Get-MgDirectoryRole -All)) { foreach ($m in (Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All)) { [pscustomobject]@{ Role=$role.DisplayName MemberType=($m.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.','') DisplayName=$m.AdditionalProperties['displayName'] Upn=$m.AdditionalProperties['userPrincipalName'] } } } $priv | Export-Csv (Join-Path $outDir 'privileged-roles.csv') -NoTypeInformation # --- Dormancy and guests --- $props = 'id','displayName','userPrincipalName','accountEnabled', 'createdDateTime','userType','signInActivity' $users = Get-MgUser -All -Property $props $cutoff = (Get-Date).AddDays(-90) $dormant = $users | Where-Object { $_.AccountEnabled -and ($null -eq $_.SignInActivity.LastSignInDateTime -or $_.SignInActivity.LastSignInDateTime -lt $cutoff) } $guests = $users | Where-Object { $_.UserType -eq 'Guest' } $dormant | Select-Object DisplayName,UserPrincipalName, @{n='LastSignIn';e={$_.SignInActivity.LastSignInDateTime}} | Export-Csv (Join-Path $outDir 'dormant.csv') -NoTypeInformation # --- Summary --- $total = $users.Count $summary = [ordered]@{ 'Report generated' = (Get-Date).ToString('u') 'Total users' = $total 'MFA-capable' = "$($mfaCapable.Count) ($([math]::Round(($mfaCapable.Count/[math]::Max($total,1))*100))%)" 'SMS-reliant (migrate)' = $smsReliant.Count 'Privileged assignments'= $priv.Count 'Dormant (90d+)' = $dormant.Count 'Guest accounts' = $guests.Count 'Evidence folder' = $outDir } "`n===== NORTHGATE IDENTITY RISK BASELINE =====" | Write-Host -ForegroundColor Cyan $summary.GetEnumerator() | ForEach-Object { '{0,-24}: {1}' -f $_.Key, $_.Value | Write-Host }
NorthgateBaseline-<timestamp> containing registration.csv, privileged-roles.csv and dormant.csv, plus a cyan summary block in the console showing counts. Open registration.csv and confirm one row per user.
What just happened? You produced the exact artefact Northgate's programme board asked for: a dated, repeatable measure of MFA coverage, SMS reliance, privileged sprawl and dormancy. Re-running it next week gives you a trend line, which is how you prove remediation is working.
Testing and Validation
End-to-end test
- Run the full baseline script against your seeded tenant.
- Confirm the summary shows four or more total users and at least one privileged assignment.
- Register the Microsoft Authenticator app for
asmithviaaka.ms/mfasetup, then re-run the script.IsMfaCapablefor asmith flips to true and the MFA-capable percentage rises. This proves the report reflects real change.
Common failure modes
| Symptom | Cause | Resolution |
|---|---|---|
Insufficient privileges on the registration report | Session lacks AuditLog.Read.All | Reconnect with the full read scope set |
signInActivity is always null | Tenant has no P1/P2 licence, or the property was not requested | Assign the P2 trial licence and include signInActivity in -Property |
Get-MgReport... not recognised | Reports sub-module not loaded | Run Import-Module Microsoft.Graph.Reports or reinstall the meta-module |
| Registration report is empty | New tenant, report data can lag up to a few hours | Wait and re-run, or validate against the seeded user count first |
Security Analysis
What makes this implementation sound
- Least privilege by construction: assessment runs on read-only scopes. Write capability exists for a single, isolated seeding step and is dropped immediately after.
- Evidence integrity: each run writes to its own timestamped folder, so baselines are never overwritten and remain audit-defensible.
- Authoritative sources: MFA capability comes from the registration report, not from guesswork or legacy per-user MFA state.
What is intentionally simplified for the lab
- Interactive delegated sign-in is used instead of an app registration with certificate authentication. Convenient for hands-on work, replaced in Lab 11 for unattended runs.
- The dormancy threshold is a flat 90 days with no new-joiner guard. Production needs per-population thresholds and exclusions.
- Role holders are read from active directory role membership. Eligible-but-not-active PIM assignments are covered in Lab 07.
Production hardening recommendations
- Run baselines from a dedicated automation identity with only the application read permissions listed here, per NIST SP 800-53 AC-6 (least privilege).
- Store CSV evidence in an access-controlled, immutable location aligned to your retention policy.
- Schedule the baseline and alert on regressions, for example a rise in dormant or SMS-reliant counts.
Cleanup Instructions
Remove the seeded personas and disconnect
Connect-MgGraph -Scopes 'User.ReadWrite.All' -NoWelcome
$domain = (Get-MgOrganization).VerifiedDomains | Where-Object IsDefault | Select-Object -ExpandProperty Name
foreach ($alias in 'asmith','jpatel','lokafor','mreeves') {
$u = Get-MgUser -Filter "userPrincipalName eq '$alias@$domain'" -ErrorAction SilentlyContinue
if ($u) { Remove-MgUser -UserId $u.Id; Write-Host "Removed $alias" }
}
# End the Graph session.
Disconnect-MgGraph
Get-MgContext returns nothing after Disconnect-MgGraph. The CSV evidence folders on disk are yours to keep or delete, they contain no secrets, only object metadata.
Recommended Learning Links
- Microsoft Graph PowerShell SDK documentation
- Authentication methods activity and registration report
- Sign up for Microsoft Entra ID P1 or P2 editions
- Graph API: authentication method registration details resource
- Manage inactive user accounts with signInActivity
- Entra built-in role template IDs reference
Key Takeaways and Next Lab
- Every hardening programme begins with a measured, read-only baseline, not with policy changes.
- The authentication methods registration report is the authoritative source of MFA and SSPR coverage.
- Least privilege applies to your own tooling: assess with read scopes, elevate only for the exact write step.
- Dated CSV evidence turns a hardening effort into a defensible, board-ready trend line.
SMS-reliant and not MFA-capable populations this lab identified and drive them onto Microsoft Authenticator and phishing-resistant methods using the Authentication Methods policy, then re-run this baseline to prove the shift.
Identity Bytes // IB-ENTRA-SEC Track // Lab 01 of 12. British English. For lab and training use against a disposable tenant only.