All scripts
Governance 421
Microsoft 365 Group Expiration Report
A Microsoft Graph script that reports on Microsoft 365 Groups nearing their expiration date, and flags any that have no owner left to receive the renewal notice.
Get-M365GroupExpirationReport.ps1
<#
.SYNOPSIS
Reports on Microsoft 365 Groups that are approaching or past their expiration date.
.DESCRIPTION
Connects to Microsoft Graph and pulls every Microsoft 365 Group covered by a
group lifecycle policy, then reports how many days are left before each one
expires or is renewed. Useful for catching groups that are about to be
deleted with nobody watching, or groups where the last owner left the
company and renewal emails are going nowhere.
.PARAMETER DaysUntilExpiration
Only show groups expiring within this many days. Defaults to 30.
.PARAMETER ExportPath
Optional path to export the results as a CSV file.
.EXAMPLE
.\Get-M365GroupExpirationReport.ps1 -DaysUntilExpiration 30
.EXAMPLE
.\Get-M365GroupExpirationReport.ps1 -DaysUntilExpiration 60 -ExportPath C:\Reports\GroupExpiration.csv
.AUTHOR
Shehryar Hassan
#>
[CmdletBinding()]
param(
[int]$DaysUntilExpiration = 30,
[string]$ExportPath
)
if (-not (Get-MgContext)) {
Connect-MgGraph -Scopes "Group.Read.All", "Directory.Read.All"
}
Write-Host "Pulling Microsoft 365 Groups with expiration data..." -ForegroundColor Cyan
$groups = Get-MgGroup -All -Property "Id,DisplayName,Mail,ExpirationDateTime,RenewedDateTime,GroupTypes" |
Where-Object { $_.GroupTypes -contains "Unified" -and $_.ExpirationDateTime }
if (-not $groups) {
Write-Host "No groups with an expiration date were found. Check that a group lifecycle policy is configured in the Entra admin center." -ForegroundColor Yellow
return
}
$today = Get-Date
$results = foreach ($group in $groups) {
$expires = [datetime]$group.ExpirationDateTime
$daysLeft = [math]::Round(($expires - $today).TotalDays)
if ($daysLeft -le $DaysUntilExpiration) {
$owners = Get-MgGroupOwner -GroupId $group.Id -All
$ownerNames = if ($owners) {
($owners | ForEach-Object { $_.AdditionalProperties["displayName"] }) -join "; "
}
else {
"NO OWNERS"
}
[PSCustomObject]@{
GroupName = $group.DisplayName
Mail = $group.Mail
ExpiresOn = $expires.ToString("yyyy-MM-dd")
DaysLeft = $daysLeft
LastRenewed = if ($group.RenewedDateTime) { ([datetime]$group.RenewedDateTime).ToString("yyyy-MM-dd") } else { "Never" }
Owners = $ownerNames
Status = if ($daysLeft -lt 0) { "EXPIRED" } elseif ($ownerNames -eq "NO OWNERS") { "AT RISK, NO OWNER" } else { "EXPIRING SOON" }
}
}
}
$results = $results | Sort-Object DaysLeft
if (-not $results) {
Write-Host "No groups are expiring within the next $DaysUntilExpiration days." -ForegroundColor Green
return
}
$results | Format-Table -AutoSize
$atRisk = $results | Where-Object { $_.Status -eq "AT RISK, NO OWNER" }
if ($atRisk) {
Write-Host ""
Write-Host "$($atRisk.Count) group(s) are expiring soon with no owner to receive the renewal notice. Fix ownership on these first." -ForegroundColor Red
}
if ($ExportPath) {
$results | Export-Csv -Path $ExportPath -NoTypeInformation
Write-Host "Exported to $ExportPath" -ForegroundColor Cyan
}
Read it before you run it, and test in a safe tenant first.