Skip to content

Commit 95d9777

Browse files
authored
Merge pull request #923 from ojopiyo/patch-17
Create readme.md
2 parents fa55c33 + 5b865a1 commit 95d9777

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed
328 KB
Loading
58.7 KB
Loading
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[
2+
{
3+
"name": "mfa-enforcement-gaps",
4+
"source": "pnp",
5+
"title": "MFA Enforcement Gaps",
6+
"shortDescription": "This script identifies users in a Microsoft 365 tenant who are not effectively protected by Multi-Factor Authentication (MFA).",
7+
"url": "https://pnp.github.io/script-samples/mfa-enforcement-gaps/README.html",
8+
"longDescription": [
9+
""
10+
],
11+
"creationDateTime": "2026-01-21",
12+
"updateDateTime": "2026-01-21",
13+
"products": [
14+
"Graph"
15+
],
16+
"metadata": [
17+
{
18+
"key": "GRAPH-POWERSHELL",
19+
"value": "1.0.0"
20+
}
21+
],
22+
"categories": [
23+
"Report",
24+
"Security"
25+
],
26+
"tags": [
27+
"Get-MgConditionalAccessPolicy"
28+
],
29+
"thumbnails": [
30+
{
31+
"type": "image",
32+
"order": 100,
33+
"url": "https://raw.githubusercontent.com/pnp/script-samples/main/scripts/mfa-enforcement-gaps/assets/preview.png",
34+
"alt": "Preview of the sample MFA Enforcement Gaps"
35+
}
36+
],
37+
"authors": [
38+
{
39+
"gitHubAccount": "ojopiyo",
40+
"company": "",
41+
"pictureUrl": "https://github.com/ojopiyo.png",
42+
"name": "Josiah Opiyo"
43+
}
44+
],
45+
"references": [
46+
{
47+
"name": "Want to learn more about Microsoft Graph PowerShell SDK and the cmdlets",
48+
"description": "Check out the Microsoft Graph PowerShell SDK documentation site to get started and for the reference to the cmdlets.",
49+
"url": "https://learn.microsoft.com/graph/powershell/get-started"
50+
}
51+
]
52+
}
53+
]
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# MFA Enforcement Gaps
2+
3+
## Professional Summary
4+
5+
This script identifies users in a Microsoft 365 tenant who are not effectively protected by Multi-Factor Authentication (MFA). It highlights accounts with no registered MFA methods and flags users explicitly excluded from Conditional Access (CA) policies, providing a clear view of MFA enforcement gaps across the organisation.
6+
7+
The output supports security hardening, audit readiness, and governance decision-making with minimal operational overhead.
8+
9+
## Why It Matters
10+
11+
In many tenants, MFA is assumed to be “on” globally, yet exceptions quietly accumulate over time:
12+
legacy service accounts, temporary exclusions, break-glass users, or staff onboarded before policy maturity.
13+
14+
These gaps are often discovered **only during incidents or audits**.
15+
16+
This script surfaces those risks proactively, allowing security teams to:
17+
- Detect silent exposure before compromise
18+
- Validate Conditional Access design assumptions
19+
- Demonstrate effective access controls to auditors and stakeholders
20+
21+
## Benefits
22+
23+
- Protects work accounts by exposing users without strong authentication
24+
- Reduces cyber risk by identifying high-impact misconfigurations
25+
- Improves compliance posture aligned with ISO 27001, GDPR best practices, and Zero Trust
26+
- Strengthens defence in depth by validating MFA + Conditional Access together
27+
- Improves audit outcomes with evidence-based reporting
28+
- Builds confidence in identity security controls with measurable insight
29+
30+
# [Graph PowerShell SDK](#tab/graphps)
31+
32+
```powershell
33+
34+
$ErrorActionPreference = 'Stop'
35+
36+
# ==============================
37+
# Authentication Configuration
38+
# ==============================
39+
40+
$TenantId = "" # e.g. contoso.onmicrosoft.com
41+
$ClientId = "" # App Registration Client ID
42+
$Thumbprint = "" # Certificate Thumbprint
43+
44+
$requiredScopes = @(
45+
'User.Read.All',
46+
'Directory.Read.All',
47+
'Policy.Read.All',
48+
'AuthenticationMethod.Read.All'
49+
)
50+
51+
# ==============================
52+
# Connect to Microsoft Graph
53+
# ==============================
54+
55+
$graphContext = Get-MgContext
56+
57+
if (-not $graphContext) {
58+
59+
if ($TenantId -and $ClientId -and $Thumbprint) {
60+
61+
Connect-MgGraph `
62+
-TenantId $TenantId `
63+
-ClientId $ClientId `
64+
-CertificateThumbprint $Thumbprint `
65+
-NoWelcome
66+
67+
} else {
68+
69+
Connect-MgGraph `
70+
-Scopes $requiredScopes `
71+
-NoWelcome
72+
}
73+
}
74+
75+
# ==============================
76+
# Data Collection
77+
# ==============================
78+
79+
$users = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UserType
80+
81+
$enabledCAPolicies = Get-MgConditionalAccessPolicy -All |
82+
Where-Object { $_.State -eq 'enabled' }
83+
84+
$excludedUserIds = $enabledCAPolicies |
85+
ForEach-Object { $_.Conditions.Users.ExcludeUsers } |
86+
Where-Object { $_ } |
87+
Select-Object -Unique
88+
89+
# ==============================
90+
# MFA Evaluation
91+
# ==============================
92+
93+
$results = foreach ($user in $users) {
94+
95+
if (-not $user.AccountEnabled -or $user.UserType -ne 'Member') {
96+
continue
97+
}
98+
99+
$authMethods = Get-MgUserAuthenticationMethod -UserId $user.Id
100+
101+
$hasStrongMfa = $authMethods |
102+
Where-Object {
103+
$_.'@odata.type' -match 'microsoftAuthenticator|fido2|phoneAuthenticationMethod'
104+
}
105+
106+
[PSCustomObject]@{
107+
DisplayName = $user.DisplayName
108+
UserPrincipalName = $user.UserPrincipalName
109+
MFARegistered = [bool]$hasStrongMfa
110+
ExcludedFromCA = $excludedUserIds -contains $user.Id
111+
RiskStatus = if (-not $hasStrongMfa -or ($excludedUserIds -contains $user.Id)) {
112+
'MFA Gap'
113+
} else {
114+
'Protected'
115+
}
116+
}
117+
}
118+
119+
# ==============================
120+
# Output
121+
# ==============================
122+
123+
$results |
124+
Where-Object { $_.RiskStatus -eq 'MFA Gap' } |
125+
Sort-Object ExcludedFromCA, MFARegistered |
126+
Export-Csv -Path '.\MFA-Enforcement-Gaps.csv' -NoTypeInformation -Encoding UTF8
127+
128+
129+
```
130+
[!INCLUDE [More about Microsoft Graph PowerShell SDK](../../docfx/includes/MORE-GRAPHSDK.md)]
131+
***
132+
133+
134+
## Usage
135+
1. Run from a secure admin workstation
136+
2. Ensure the executing identity has:
137+
- Global Reader or Security Reader
138+
- Graph API consent for required scopes
139+
3. Review the generated CSV:
140+
- Prioritise users excluded from Conditional Access
141+
- Validate whether exclusions are intentional and justified
142+
143+
## Output
144+
**MFA-Enforcement-Gaps.csv**
145+
146+
| Column | Description |
147+
|-----------|-----------|
148+
| DisplayName | User display name |
149+
| UserPrincipalName | Sign-in identity |
150+
| MFARegistered | Strong MFA methods detected |
151+
| ExcludedFromCA | Explicit CA exclusion |
152+
| RiskIndicator | MFA Gap / Protected |
153+
154+
## Notes
155+
156+
- This script focuses on effective MFA protection, not legacy per-user MFA states
157+
- Conditional Access exclusions represent the highest risk signal
158+
- Designed to scale across large tenants using Graph paging and filtering
159+
- Suitable for scheduled execution and security reporting pipelines
160+
161+
162+
## Contributors
163+
164+
| Author(s) |
165+
|-----------|
166+
| [Josiah Opiyo](https://github.com/ojopiyo) |
167+
168+
*Built with a focus on automation, governance, least privilege, and clean Microsoft 365 tenants—helping M365 admins gain visibility and reduce operational risk.*
169+
170+
171+
## Version history
172+
173+
Version|Date|Comments
174+
-------|----|--------
175+
1.0|Jan 21, 2026|Initial release
176+
177+
178+
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]
179+
<img src="https://m365-visitor-stats.azurewebsites.net/script-samples/scripts/mfa-enforcement-gaps" aria-hidden="true" />

0 commit comments

Comments
 (0)