Entra / Microsoft 365 · Applications
Get tenant feature updates (Graph)
Use the Microsoft 365 Service Communications Graph API to retrieve feature updates planned for a tenant.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
# Review required modules and connection steps before running.# Connect to Microsoft Graph or Exchange Online as needed for this script.
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "",[string] $AppId = "")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"}}Function ConvertFrom-Html{[CmdletBinding(SupportsShouldProcess = $True)]Param([Parameter(Mandatory=$true, Position=0)][string]$Html)$HtmlObject = New-Object -Com "HTMLFile"$HtmlObject.IHTMLDocument2_write($Html)return $HtmlObject.documentElement.innerText}$CSVOutputFile = "C:\temp\MessagesRequiringAction.csv"$HTMLOutputFile = "C:\temp\MessagesRequiringAction.Html"$Now = Get-Date# Change these values to match your tenant and app details$AppSecret = '12EJ.O2~1.HUFJXRcJ-8o4S2e~q_16-YJw'# Construct URI and body needed for authentication$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"$body = @{client_id = $AppIdscope = "https://graph.microsoft.com/.default"client_secret = $AppSecretgrant_type = "client_credentials"}# Get OAuth 2.0 Token$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing# Unpack Access Token$token = ($tokenRequest.Content | ConvertFrom-Json).access_token$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $Token" }# Get feature updates with action required within the next 180 days - the filter clause sets the number of days to look back$DaysRange = (Get-Date).AddDays(180)$DaysRangeZ = Get-Date($DaysRange) -format s# Fetch messages and filter to get just those which have an action required date set$Uri = "https://graph.microsoft.com/beta/admin/serviceAnnouncement/messages?`$filter=actionRequiredByDateTime le $DaysRangeZ" + "Z"[array]$Messages = Get-GraphData -AccessToken $Token -Uri $uriIf (!($Messages)) {Write-Host "No update messages found with action required in the next 180 days - exiting"; break}# Make sure that the messages are sorted$Messages = $Messages | Sort {$_.actionRequiredByDateTime -as [datetime]} -Descending$MessageData = [System.Collections.Generic.List[Object]]::new()# Go through the messages and extract information of interestForEach ($Message in $Messages) {$Status = "Action due"$Tags = $Message.Tags -join ", "$Services = $Message.Services -join ", "If ($Message.actionRequiredByDateTime) {$TimeToGo = New-TimeSpan ($Message.actionRequiredByDateTime)$FormattedTime = "{0:dd}d:{0:hh}h:{0:mm}m" -f $TimeToGo[datetime]$MessageDate = $Message.actionRequiredByDateTimeIf ($Now -ge $MessageDate) {$Status = "Action overdue"$FormattedTime = $FormattedTime + " (o/d)" }}Else { $FormattedTime = "N/A" }$RoadmapId = $Null; $BlogLink = $Null; $WebLink = $NullIf ($Message.Details -ne $Null) {$RoadmapId = $Message.Details |?{$_.Name -eq "RoadmapIds"} | Select -ExpandProperty Value$BlogLink = $Message.Details |?{$_.Name -eq "BlogLink"} | Select -ExpandProperty Value$WebLink = $Message.Details |?{$_.Name -eq "ExternalLink"} | Select -ExpandProperty Value }$Description = ConvertFrom-html -html $Message.Body.Content$ReportLine = [PSCustomObject][Ordered]@{Title = $Message.TitleId = $Message.IdServices = $ServicesCategory = $Message.CategorySeverity = $Message.SeverityActionBy = Get-Date($Message.actionRequiredByDateTime) -format gTimeToGo = $FormattedTimeStatus = $StatusStartDate = Get-Date($Message.startDateTime) -format gEndDate = Get-Date($Message.endDateTime) -format gLastUpdate = Get-Date($Message.lastModifiedDateTime) -format gDescription = $DescriptionRoadmapId = $RoadmapIdBlogLink = $BlogLinkWebLink = $WebLinkTags = $Tags }$MessageData.Add($ReportLine)} # End ForEach# Check that we are connected to Exchange Online$ModulesLoaded = Get-Module | Select NameIf (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Please connect to the Exchange Online Management module and then restart the script"; break}# OK, we seem to be fully connected to Exchange Online, so we can fetch the organization name (to make the report prettier)$OrgName = (Get-OrganizationConfig).Name# Create the HTML report$htmlhead="<html><style>BODY{font-family: Arial; font-size: 8pt;}H1{font-size: 22px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H2{font-size: 18px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H3{font-size: 16px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;}TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}TD{border: 1px solid #969595; padding: 5px; }td.pass{background: #B7EB83;}td.warn{background: #FFF275;}td.fail{background: #FF2626; color: #ffffff;}td.info{background: #85D4FF;}</style><body><div align=center><p><h1>Microsoft 365 Features Action Required Report</h1></p><p><h2><b>for the " + $Orgname + " organization</b></h2></p><p><h3>Generated: " + (Get-Date -format g) + "</h3></p></div>"$htmlbody1 = $MessageData | ConvertTo-Html -Fragment$htmltail = "<p>Report created for: " + $OrgName + "</p>" +"<p>Created: " + $Now + "<p>"$htmlreport = $htmlhead + $htmlbody1 + $htmltail$htmlreport | Out-File $HTMLOutputFile -Encoding UTF8$MessageData | Export-CSV -NoTypeInformation $CSVOutputFileCLS# And report outWrite-Host "All done. Output files are" $CSVOutputFile "and" $HTMLOutputFile
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.Attribution
Author
Office365itpros