Entra / Microsoft 365 · Groups
Azure Automation: groups expiration report
Reports Microsoft 365 groups approaching expiration using Azure Automation and Microsoft Graph.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -ClientID $Connection.ApplicationId -TenantId $Connection.TenantId -CertificateThumbprint $Connection.CertificateThumbprint
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
function Get-GraphData {# Based on https://danielchronlund.com/2018/11/19/fetch-data-from-microsoft-graph-with-powershell-paging-support/# GET data from Microsoft Graph.param ([parameter(Mandatory = $true)]$AccessToken,[parameter(Mandatory = $true)]$Uri)# Check if authentication was successful.if ($AccessToken) {$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $AccessToken"'ConsistencyLevel' = "eventual" }# Create an empty array to store the result.$QueryResults = @()# Invoke REST method and fetch data until there are no pages left.do {$Results = ""$StatusCode = ""do {try {$Results = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method "GET" -ContentType "application/json"$StatusCode = $Results.StatusCode} catch {$StatusCode = $_.Exception.Response.StatusCode.value__if ($StatusCode -eq 429) {Write-Warning "Got throttled by Microsoft. Sleeping for 45 seconds..."Start-Sleep -Seconds 45}else {Write-Error $_.Exception}}} while ($StatusCode -eq 429)if ($Results.value) {$QueryResults += $Results.value}else {$QueryResults += $Results}$uri = $Results.'@odata.nextlink'} until (!($uri))# Return the result.$QueryResults}else {Write-Error "No Access Token"}}# Set up our connections$Connection = Get-AutomationConnection -Name AzureRunAsConnection$Certificate = Get-AutomationCertificate -Name AzureRunAsCertificate$GraphConnection = Get-MsalToken -ClientCertificate $Certificate -ClientId $Connection.ApplicationID -TenantId $Connection.TenantID$Token = $GraphConnection.AccessToken$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $Token"'ConsistencyLevel' = "eventual" }$AzConnection = Connect-AzAccount -Tenant $Connection.TenantId -ApplicationId `$Connection.ApplicationId -CertificateThumbPrint $Connection.CertificateThumbprint `-ServicePrincipalWrite-Host "AZ Connection is" $AzConnection# Get username and password from Key Vault - You need to set up your own Key Vault and populate# it with secrets to make this work...$UserName = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -Name "ExoAccountName" -AsPlainText$UserPassword = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "ExoAccountPassword" -AsPlainText# Create credentials object from the username and password[securestring]$SecurePassword = ConvertTo-SecureString $UserPassword -AsPlainText -Force[pscredential]$UserCredentials = New-Object System.Management.Automation.PSCredential ($UserName, $SecurePassword)# Get Site URL to use with PnP connection$SiteURL = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "SPOSiteURL" -AsPlainText# Target channel identifier for incoming webhook connector$TargetChannel = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "IncomingWebhookId" -AsPlainText# Target team and channel in that team to which we post a message containing the report$TargetTeamId = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "TargetTeamId" -AsPlainText$TargetTeamChannel = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "TargetChannelID" -AsPlainTextWrite-Output "Credentials" $UserCredentialsWrite-Output "Target Site" $SiteURLWrite-Output "Target team and channel" $TargetTeamId " " $TargetTeamChannel# Get set of groups with an expiration date set. Can't check for null as the ExpirationDateTime property doesn't support this$uri = "https://graph.microsoft.com/beta/groups?`$filter=ExpirationDateTime ge 2014-01-01T00:00:00Z AND groupTypes/any(a:a eq 'unified')&`$count=true"[array]$Groups = Get-GraphData -AccessToken $Token -Uri $uriIf (!($Groups)) {Write-Output "No groups found subject to the expiration policy - exiting" ; break}$Report = [System.Collections.Generic.List[Object]]::new(); $Today = (Get-Date)ForEach ($G in $Groups) {$Days = (New-TimeSpan -Start $G.CreatedDateTime -End $Today).Days # Age of group#$LastRenewed = $G.RenewedDateTime#$NextRenewalDue = $G.ExpirationDateTime$DaysLeft = (New-TimeSpan -Start $Today -End $G.ExpirationDateTime).Days$GroupsInPolicy++$ReportLine = [PSCustomObject]@{Group = $G.DisplayNameCreated = Get-Date($G.CreatedDateTime) -format g"Age in days" = $Days"Last renewed" = Get-Date($G.RenewedDateTime) -format g"Next renewal" = Get-Date($G.ExpirationDateTime) -format g"Days before expiration" = $DaysLeft}$Report.Add($ReportLine)} # End ForeachWrite-Output "Total Groups covered by expiration policy:" $Groups.CountWrite-Output ""# Write out details to show that the job works!$Report | Sort-Object "Days before expiration" | Format-Table Group, "Last renewed", "Next renewal", "Days before expiration" -AutoSize# Create data to store in SharePoint Online# First, the CSV file$SDate = Get-Date -format yyyyMMddHHmmss[string]$SourceDocument = "Microsoft 365 Groups Expiration Report " + $SDate + ".csv"[string]$HTMLDocument = "Microsoft 365 Groups Expiration Report " + $SDate + ".html"$Report | Sort-Object "Days before expiration" | Export-CSV -NoTypeInformation $SourceDocument# Connect to SharePoint Online using PnP$PnpConnection = Connect-PnPOnline $SiteURL -Credentials $UserCredentials -ReturnConnection# Add a document title$Values = @{"Title" = 'Microsoft 365 Groups Expiration Report (CSV)'}# Add the file to the General folder$FileAddStatus = (Add-PnPFile -Folder "Shared Documents/General" -Path $SourceDocument -Connection $PnpConnection -Values $Values | Out-Null)$FileAddStatus$NewFileUri = $SiteUrl + "/Shared Documents/General/" + $HTMLDocument# Now the HTML fileConnect-MgGraph -ClientID $Connection.ApplicationId -TenantId $Connection.TenantId -CertificateThumbprint $Connection.CertificateThumbprint$Organization = Get-MgOrganization$TenantName = $Organization.DisplayName$Title = "Microsoft 365 Groups Expiration Report"$cssString = @'<style type="text/css">.tftable {table-layout:fixed;width: 40%;font-family:"Segoe UI";font-size:12px;color:#333333;border-width: 1px;border-color: #729ea5;border-collapse: collapse;}.tftable th {width: 30%;font-size:12px;background-color:#acc8cc;border-width: 1px;padding: 8px;border-style: solid;border-color: #729ea5;text-align:left;}.tftable tr {background-color:#d4e3e5;}.tftable td {width: 10%font-size:12px;border-width: 1px;padding: 8px;border-style: solid;border-color: #729ea5;}.tftable tr:hover {background-color:#ffffff;}table.center {margin-left: auto;margin-right: auto;}</style>'@$Body = "<html><head><title>$($Title)</title>"$Body += '<meta http-equiv="Content-Type content="text/html; charset=ISO-8859-1 />'$Body += $cssString$Body += '</head><body><p><font face="Segoe UI"><h1>Microsoft 365 Groups Expiration Report</h1></font></p><p><font face="Segoe UI"><h2>Tenant: ' + ($TenantName) + '</h2></p><p><font face="Segoe UI"><h3>Generated: ' + $Today + '</h3></font></p>'$Body += '<table class="tftable">'$Body += "<colgroup><col/><col/><col/><col/><col/><col/></colgroup> <tr><th>Group</th><th>Created</th><th>Age in days</th><th>Last renewed</th><th>Next renewal</th><th>Days before expiration</th></tr>"$Report = $Report | Sort-Object GroupForEach ($R in $Report) {$Body += "<tr><td>$($R.Group)</td><td>$($R.Created)</td><td>$($R.'Age in days')</td><td>$($R.'Last Renewed')</td><td>$($R.'Next Renewal')</td><td>$($R.'Days before expiration')</td></tr>"}$Body += "</table>"$Body += '<p><font face="Segoe UI"><h3>End of Report<h3></font></p>'$Body += '<p><font size="2" face="Segoe UI">'$Body += '</body></html>'$Body | Out-File $HTMLDocument# And write to SharePoint Online$Values = @{"Title" = 'New Microsoft 365 Groups Expiration Report (HTML)'}$FileAddStatus = (Add-PnPFile -Folder "Shared Documents/General" -Path $HTMLDocument -Connection $PnpConnection -Values $Values | Out-Null)# Post to a Teams channel using an incoming webhook connector$GroupWebHookData = 'The new report is available in <a href="' + $NewFileUri + '">' + 'Microsoft 365 Groups Expiration Report</a>'Write-Host $GroupWebHookData$DateNow = Get-Date -format g$Notification = @"{"@type": "MessageCard","@context": "https://schema.org/extensions","summary": "Microsoft 365 Groups","themeColor": "0072C6","title": "Notification: New Microsoft 365 Groups Expiration Report is available","sections": [{"facts": [{"name": "Tenant:","value": "TENANT"},{"name": "Date:","value": "DATETIME"}],"markdown" : "true"}],"potentialAction": [{"@type": "OpenUri","name": "Download the report","targets": [{"os": "default","uri": "URI"}],} ]}"@$NotificationBody = $Notification.Replace("TENANT","$TenantName").Replace("DATETIME","$DateNow").Replace("URI","$NewFileUri")$Command = (Invoke-RestMethod -uri $TargetChannel -Method Post -body $NotificationBody -ContentType 'application/json')Write-Host "Command result" $Command# Post to a Teams channel using PnPSubmit-PnPTeamsChannelMessage -Team $TargetTeamId -Channel $TargetTeamChannel -Message $Body -ContentType Html -ImportantDisconnect-PnpOnline
Attribution
Author
Office365itpros