Skip to content

Commit 888752b

Browse files
authored
Add files via upload
1 parent 80e8276 commit 888752b

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Report-InactiveGuestUsers-150Days.ps1
2+
# Purpose:
3+
# Read-only report of Microsoft Entra guest users inactive for more than X days.
4+
# Exports CSV sorted from oldest sign-in to newest sign-in.
5+
#
6+
# Default threshold: 150 days
7+
# Recommended action output: Block sign-ins for these users after review.
8+
#
9+
# Required Graph scopes:
10+
# User.Read.All
11+
# AuditLog.Read.All
12+
# Directory.Read.All
13+
#
14+
# Notes:
15+
# - This script does NOT block or delete users.
16+
# - Last sign-in data depends on signInActivity availability in your tenant/licensing.
17+
# - Guests with no sign-in date are included as "Never signed in" if older than threshold by CreatedDateTime.
18+
19+
param(
20+
[int]$InactiveDays = 150,
21+
[string]$OutputPath = ".\InactiveGuestUsers_150Days.csv"
22+
)
23+
24+
$ErrorActionPreference = "Stop"
25+
26+
function Write-Section {
27+
param([string]$Text)
28+
29+
Write-Host ""
30+
Write-Host "============================================================" -ForegroundColor DarkGray
31+
Write-Host " $Text" -ForegroundColor Cyan
32+
Write-Host "============================================================" -ForegroundColor DarkGray
33+
}
34+
35+
function Write-Ok {
36+
param([string]$Text)
37+
Write-Host "[OK] $Text" -ForegroundColor Green
38+
}
39+
40+
function Write-Info {
41+
param([string]$Text)
42+
Write-Host "[INFO] $Text" -ForegroundColor Gray
43+
}
44+
45+
function Write-Warn {
46+
param([string]$Text)
47+
Write-Host "[WARN] $Text" -ForegroundColor Yellow
48+
}
49+
50+
function Write-Bad {
51+
param([string]$Text)
52+
Write-Host "[ACTION] $Text" -ForegroundColor Red
53+
}
54+
55+
Write-Section "Inactive Microsoft Entra Guest User Report"
56+
57+
$ThresholdDate = (Get-Date).AddDays(-$InactiveDays)
58+
59+
Write-Info "Inactive threshold: $InactiveDays days"
60+
Write-Info "Threshold date: $ThresholdDate"
61+
Write-Info "Output CSV: $OutputPath"
62+
63+
Write-Section "Connecting to Microsoft Graph"
64+
65+
Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All","Directory.Read.All" -NoWelcome
66+
67+
$ctx = Get-MgContext
68+
Write-Ok "Connected to tenant: $($ctx.TenantId)"
69+
Write-Info "Scopes: $($ctx.Scopes -join ', ')"
70+
71+
Write-Section "Retrieving guest users"
72+
73+
Write-Info "Querying users where userType eq 'Guest'..."
74+
Write-Info "Retrieving DisplayName, UPN, Mail, CreatedDateTime, AccountEnabled, SignInActivity, ExternalUserState."
75+
76+
$Guests = Get-MgUser `
77+
-Filter "userType eq 'Guest'" `
78+
-Property "id,displayName,userPrincipalName,mail,userType,accountEnabled,createdDateTime,externalUserState,signInActivity" `
79+
-All
80+
81+
Write-Ok "Total guest users found: $($Guests.Count)"
82+
83+
Write-Section "Evaluating inactivity"
84+
85+
$Report = foreach ($Guest in $Guests) {
86+
87+
$LastSuccessful = $Guest.SignInActivity.LastSuccessfulSignInDateTime
88+
$LastInteractive = $Guest.SignInActivity.LastSignInDateTime
89+
$LastNonInteractive = $Guest.SignInActivity.LastNonInteractiveSignInDateTime
90+
91+
# Prefer LastSuccessfulSignInDateTime when available.
92+
# Fall back to LastSignInDateTime if LastSuccessfulSignInDateTime is empty.
93+
$EffectiveLastSignIn = $null
94+
95+
if ($LastSuccessful) {
96+
$EffectiveLastSignIn = [datetime]$LastSuccessful
97+
}
98+
elseif ($LastInteractive) {
99+
$EffectiveLastSignIn = [datetime]$LastInteractive
100+
}
101+
102+
$CreatedDate = if ($Guest.CreatedDateTime) { [datetime]$Guest.CreatedDateTime } else { $null }
103+
104+
$Inactive = $false
105+
$DaysSinceLastSignIn = $null
106+
$InactivityReason = $null
107+
$SortDate = $null
108+
109+
if ($EffectiveLastSignIn) {
110+
$DaysSinceLastSignIn = ((Get-Date) - $EffectiveLastSignIn).Days
111+
112+
if ($EffectiveLastSignIn -lt $ThresholdDate) {
113+
$Inactive = $true
114+
$InactivityReason = "Last sign-in older than $InactiveDays days"
115+
$SortDate = $EffectiveLastSignIn
116+
}
117+
}
118+
else {
119+
# Never signed in. Include only if account was created before threshold date.
120+
if ($CreatedDate -and $CreatedDate -lt $ThresholdDate) {
121+
$Inactive = $true
122+
$DaysSinceLastSignIn = "Never"
123+
$InactivityReason = "Never signed in and account older than $InactiveDays days"
124+
$SortDate = [datetime]"1900-01-01"
125+
}
126+
}
127+
128+
if ($Inactive) {
129+
[PSCustomObject]@{
130+
DisplayName = $Guest.DisplayName
131+
UserPrincipalName = $Guest.UserPrincipalName
132+
Mail = $Guest.Mail
133+
AccountEnabled = $Guest.AccountEnabled
134+
ExternalUserState = $Guest.ExternalUserState
135+
CreatedDateTime = $CreatedDate
136+
LastSuccessfulSignInDateTime = $LastSuccessful
137+
LastInteractiveSignInDateTime = $LastInteractive
138+
LastNonInteractiveSignInDateTime = $LastNonInteractive
139+
EffectiveLastSignInDateTime = $EffectiveLastSignIn
140+
DaysSinceLastSignIn = $DaysSinceLastSignIn
141+
InactivityReason = $InactivityReason
142+
RecommendedAction = "Review owner/business need, then block sign-ins"
143+
ObjectId = $Guest.Id
144+
SortDate = $SortDate
145+
}
146+
}
147+
}
148+
149+
$ReportSorted = $Report |
150+
Sort-Object SortDate, DisplayName |
151+
Select-Object `
152+
DisplayName,
153+
UserPrincipalName,
154+
Mail,
155+
AccountEnabled,
156+
ExternalUserState,
157+
CreatedDateTime,
158+
LastSuccessfulSignInDateTime,
159+
LastInteractiveSignInDateTime,
160+
LastNonInteractiveSignInDateTime,
161+
EffectiveLastSignInDateTime,
162+
DaysSinceLastSignIn,
163+
InactivityReason,
164+
RecommendedAction,
165+
ObjectId
166+
167+
Write-Ok "Inactive guest users found: $($ReportSorted.Count)"
168+
169+
Write-Section "Exporting CSV"
170+
171+
if ($ReportSorted.Count -gt 0) {
172+
$ReportSorted |
173+
Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
174+
175+
Write-Ok "CSV exported successfully:"
176+
Write-Host " $OutputPath" -ForegroundColor White
177+
}
178+
else {
179+
Write-Ok "No inactive guest users found over $InactiveDays days. No CSV rows to export."
180+
181+
# Still create an empty CSV with headers for audit consistency
182+
$ReportSorted |
183+
Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
184+
185+
Write-Info "Empty CSV created for audit record:"
186+
Write-Host " $OutputPath" -ForegroundColor White
187+
}
188+
189+
Write-Section "Summary"
190+
191+
Write-Host "Guest users evaluated: " -NoNewline
192+
Write-Host $Guests.Count -ForegroundColor White
193+
194+
Write-Host "Inactive threshold: " -NoNewline
195+
Write-Host "$InactiveDays days" -ForegroundColor White
196+
197+
Write-Host "Inactive guest users found: " -NoNewline
198+
if ($ReportSorted.Count -gt 0) {
199+
Write-Host $ReportSorted.Count -ForegroundColor Red
200+
}
201+
else {
202+
Write-Host "0" -ForegroundColor Green
203+
}
204+
205+
if ($ReportSorted.Count -gt 0) {
206+
Write-Bad "Recommended action: Review these users, then block sign-ins for accounts with no current business justification."
207+
Write-Warn "This script is read-only and did NOT block sign-ins."
208+
Write-Warn "Do not delete immediately. Recommended lifecycle is: review/attest -> block sign-in -> delete after grace period."
209+
}
210+
else {
211+
Write-Ok "No action required based on the $InactiveDays-day inactivity threshold."
212+
}
213+
214+
Write-Host ""
215+
Write-Host "Suggested next step:" -ForegroundColor Cyan
216+
Write-Host " Review the CSV with app/site/group owners. For confirmed stale guests, block sign-in first, then delete later after a grace period." -ForegroundColor White
217+
218+
Write-Host ""
219+
Write-Host "Done." -ForegroundColor Green

0 commit comments

Comments
 (0)