source/public/New-MS365IncidentReport.ps1
Function New-MS365IncidentReport { [cmdletbinding(DefaultParameterSetName = 'Client Secret')] param ( [parameter()] [string] $OrganizationName, [parameter(Mandatory, ParameterSetName = 'Client Certificate')] [parameter(Mandatory, ParameterSetName = 'Certificate Thumbprint')] [parameter(Mandatory, ParameterSetName = 'Client Secret')] [guid] $ClientID, [parameter(Mandatory, ParameterSetName = 'Client Secret')] [string] $ClientSecret, [parameter(Mandatory, ParameterSetName = 'Client Certificate')] [System.Security.Cryptography.X509Certificates.X509Certificate2] $ClientCertificate, [parameter(Mandatory, ParameterSetName = 'Certificate Thumbprint')] [string] $ClientCertificateThumbprint, [parameter(Mandatory, ParameterSetName = 'Client Certificate')] [parameter(Mandatory, ParameterSetName = 'Certificate Thumbprint')] [parameter(Mandatory, ParameterSetName = 'Client Secret')] [string] $TenantID, [Parameter()] [switch] $StartFromLastRun, [Parameter()] [datetime] $LastUpdatedTime, [Parameter()] [string[]] $Workload, [parameter()] [ValidateSet('Ongoing', 'Resolved')] [string]$Status, [Parameter()] [switch] $SendEmail, [Parameter()] [string] $From, [Parameter()] [string[]] $To, [Parameter()] [string[]] $CC, [Parameter()] [string[]] $Bcc, [Parameter()] [boolean] $WriteReportToDisk = $true, [Parameter()] [boolean] $WriteRawJSONToDisk = $false, [Parameter()] [boolean] $Consolidate = $true ) Function ReplaceSmartCharacter { #https://4sysops.com/archives/dealing-with-smart-quotes-in-powershell/ param( [parameter(Mandatory)] [string]$String ) # Unicode Quote Characters $unicodePattern = @{ '[\u2019\u2018]' = "'" # Single quote '[\u201C\u201D]' = '"' # Double quote '\u00A0|\u1680|\u180E|\u2000|\u2001|\u2002|\u2003|\u2004|\u2005|\u2006|\u2007|\u2008|\u2009|\u200A|\u200B|\u202F|\u205F|\u3000|\uFEFF' = " " # Space } $unicodePattern.Keys | ForEach-Object { $stringToReplace = $_ $String = $String -replace $stringToReplace, $unicodePattern[$stringToReplace] } return $String } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $moduleInfo = Get-Module $($MyInvocation.MyCommand.ModuleName) $errorFlag = $false # $WriteReportToDisk = $true # $WriteRawJSONToDisk = $true $now = Get-Date #Region Prepare Output Directory # if ($WriteReportToDisk -eq $true) { if (isWindows) { $outputDir = ([System.IO.Path]::Combine($(Resolve-Path $Env:HOMEPATH), $($moduleInfo.Name), $($TenantID))) } else { $outputDir = ([System.IO.Path]::Combine($(Resolve-Path $Env:HOME), $($moduleInfo.Name), $($TenantID))) } if (!(Test-Path -Path $outputDir)) { $null = New-Item -ItemType Directory -Path $outputDir -Force } else { Remove-Item -Path $outputDir\* -Exclude runHistory.csv -Force -Confirm:$false } SayInfo "Output Directory: $outputDir" # } #EndRegion # Set the run times history file $runHistoryFile = ([System.IO.Path]::Combine($outputDir, "runHistory.csv" )) # Create the history file if it doesn't exist. if (!(Test-Path $RunHistoryFile) -or !(Get-Content $RunHistoryFile -Raw -ErrorAction SilentlyContinue)) { "RunTime,Status" | Set-Content -Path $RunHistoryFile -Force -Confirm:$false # Add initial entry 'OK' (which means successful) dated 30 days ago. This way there will always be a starting point. "$("{0:yyyy-MM-dd H:mm}" -f $now.AddDays(-30)),OK" | Add-Content -Path $RunHistoryFile -Force -Confirm:$false } if (!$OrganizationName) { $OrganizationName = $TenantID } SayInfo "Authentication type: $($pscmdlet.ParameterSetName)" SayInfo "Client ID: $ClientID" SayInfo "Tenant ID: $TenantID" # Get Service Communications API Token if ($pscmdlet.ParameterSetName -eq 'Client Secret') { $SecureClientSecret = New-Object System.Security.SecureString $ClientSecret.toCharArray() | ForEach-Object { $SecureClientSecret.AppendChar($_) } $OAuth = Get-MsalToken -ClientId $ClientID -ClientSecret $SecureClientSecret -TenantId $tenantID -ErrorAction Stop Sayinfo $($ClientSecret -replace $($ClientSecret.Substring(0, $ClientSecret.Length - 8)), $('X' * $($ClientSecret.Substring(0, $ClientSecret.Length - 8)).Length)) } elseif ($pscmdlet.ParameterSetName -eq 'Client Certificate') { $OAuth = Get-MsalToken -ClientId $ClientID -ClientCertificate $ClientCertificate -TenantId $tenantID -ErrorAction Stop } elseif ($pscmdlet.ParameterSetName -eq 'Certificate Thumbprint') { $OAuth = Get-MsalToken -ClientId $ClientID -ClientCertificate (Get-Item Cert:\CurrentUser\My\$($ClientCertificateThumbprint)) -TenantId $tenantID -ErrorAction Stop } $GraphAPIHeader = @{'Authorization' = "Bearer $($OAuth.AccessToken)" } # Get GraphAPI Token if ($SendEmail) { if (!$From) { SayWarning "You ask me to send an email report but you forgot to add the -From address."; return $null } if (!$To) { SayWarning "You ask me to send an email report but you forgot to add the -To address(es)."; return $null } } #Region Get Incidents $searchParam = @{ Token = ($OAuth.AccessToken); } if ($Status) { $searchParam += (@{Status = $Status }) } ## If -StartFromLastRun, this function will only get the incidents whose LastUpdatedTime is after the timestamp in "$outputDir\runHistory.csv" if ($StartFromLastRun) { SayInfo "Getting last successful run time from $RunHistoryFile." [datetime]$LastUpdatedTime = @(Import-Csv $RunHistoryFile | Where-Object { $_.Status -eq 'Ok' })[-1].RunTime } ## If -LastUpdatedTime, this function will only get the incidents whose LastUpdatedTime is after the $LastUpdatedTime datetime value. if ($LastUpdatedTime) { $searchParam += (@{LastUpdatedTime = $LastUpdatedTime }) SayInfo "Getting incidents from the last successful run time: $LastUpdatedTime" } if ($Workload) { $searchParam += (@{Workload = $Workload }) SayInfo "Workload: $($Workload -join ',')" } try { $events = @(Get-MS365Messages @searchParam -ErrorAction STOP) SayInfo "Total Incidents Retrieved: $($events.Count)" } catch { SayError "Failed to get data. $($_.Exception.Message)" $errorFlag = $true return $null } #EndRegion #Region Create Report ## Get the CSS style $css_string = Get-Content (($moduleInfo.ModuleBase.ToString()) + '\source\public\style.css') -Raw #Region Consolidate if ($Consolidate) { if ($events.Count -gt 0) { $mailSubject = "[$($organizationName)] Microsoft 365 Service Health Report" $event_id_file = "$outputDir\consolidated_report.html" $event_id_json_file = "$outputDir\consolidated_report.json" $htmlBody = [System.Collections.ArrayList]@() $null = $htmlBody.Add("<html><head><title>$($mailSubject)</title>") $null = $htmlBody.Add('<style type="text/css">') $null = $htmlBody.Add($css_string) $null = $htmlBody.Add("</style>") $null = $htmlBody.Add("</head><body>") $null = $htmlBody.Add("<hr>") $null = $htmlBody.Add('<table id="section"><tr><th><a name="summary">Summary</a></th></tr></table>') $null = $htmlBody.Add("<hr>") $null = $htmlBody.Add('<table id="data">') $null = $htmlBody.Add("<tr><th>Workload</th><th>Event ID</th><th>Classification</th><th>Status</th><th>Title</th></tr>") foreach ($event in ($events | Sort-Object Classification -Descending)) { $ticket_status = ($event.Status.substring(0, 1).toupper() + $event.Status.substring(1) -creplace '[^\p{Ll}\s]', ' $&').Trim(); $null = $htmlBody.Add("<tr><td>$($event.Service)</td> <td>" + '<a href="#' + $($event.ID) + '">' + "$($event.ID)</a></td> <td>$($event.Classification.substring(0, 1).toupper() + $event.Classification.substring(1))</td> <td>$($ticket_status)</td> <td>$($event.Title)</td></tr>") } $null = $htmlBody.Add('</table>') foreach ($event in $events | Sort-Object Classification -Descending) { $ticket_status = ($event.Status.substring(0, 1).toupper() + $event.Status.substring(1) -creplace '[^\p{Ll}\s]', ' $&').Trim(); $null = $htmlBody.Add("<hr>") $null = $htmlBody.Add('<table id="section"><tr><th><a name="' + $event.ID + '">' + $event.ID + '</a> | ' + $event.Service + ' | ' + $event.Title + '</th></tr></table>') $null = $htmlBody.Add("<hr>") $null = $htmlBody.Add('<table id="data">') $null = $htmlBody.Add('<tr><th>Status</th><td><b>' + $ticket_status + '</b></td></tr>') $null = $htmlBody.Add('<tr><th>Organization</th><td>' + $organizationName + '</td></tr>') $null = $htmlBody.Add('<tr><th>Classification</th><td>' + $($event.Classification.substring(0, 1).toupper() + $event.Classification.substring(1)) + '</td></tr>') $null = $htmlBody.Add('<tr><th>User Impact</th><td>' + $event.ImpactDescription + '</td></tr>') $null = $htmlBody.Add('<tr><th>Last Updated</th><td>' + "{0:yyyy-MM-dd H:mm}" -f [datetime]$event.lastModifiedDateTime + '</td></tr>') $null = $htmlBody.Add('<tr><th>Start Time</th><td>' + "{0:yyyy-MM-dd H:mm}" -f [datetime]$event.startDateTime + '</td></tr>') $null = $htmlBody.Add('<tr><th>End Time</th><td>' + $( if ($event.endDateTime) { "{0:yyyy-MM-dd H:mm}" -f [datetime]$event.endDateTime } else { "" } ) + '</td></tr>') $latestMessage = ($event.posts[-1].description.content) -replace "`n", "<br />" $null = $htmlBody.Add('<tr><th>Latest Message</th><td>' + $latestMessage + '</td></tr>') $null = $htmlBody.Add('</table>') $null = $htmlBody.Add('<div style="font-family: Tahoma;font-size: 10px"><a href = "#summary">(back to summary)</a></div>') } $null = $htmlBody.Add('<p><font size="2" face="Segoe UI Light"><br />') $null = $htmlBody.Add('<br />') $null = $htmlBody.Add('<a href="' + $moduleInfo.ProjectURI.ToString() + '" target="_blank">' + $moduleInfo.Name.ToString() + ' v' + $moduleInfo.Version.ToString() + ' </a><br></p>') $null = $htmlBody.Add('</body>') $null = $htmlBody.Add('</html>') $htmlBody = $htmlBody -join "`n" #convert to multiline string $htmlBody = ReplaceSmartCharacter $htmlBody if ($WriteReportToDisk -eq $true) { $htmlBody | Out-File $event_id_file -Force } if ($SendEmail -eq $true) { # Recipients $toAddressJSON = @() $To | ForEach-Object { $toAddressJSON += @{EmailAddress = @{Address = $_ } } } try { #message $mailBody = @{ message = @{ subject = $mailSubject body = @{ contentType = "HTML" content = $htmlBody } toRecipients = @( $ToAddressJSON ) internetMessageHeaders = @( @{ name = "X-Mailer" value = "MS365HealthReport (junecastillote)" } ) } } ## Add CC recipients if specified if ($Cc) { $ccAddressJSON = @() $Cc | ForEach-Object { $ccAddressJSON += @{EmailAddress = @{Address = $_ } } } $mailBody.Message += @{ccRecipients = $ccAddressJSON } } ## Add BCC recipients if specified if ($Bcc) { $BccAddressJSON = @() $Bcc | ForEach-Object { $BccAddressJSON += @{EmailAddress = @{Address = $_ } } } $mailBody.Message += @{BccRecipients = $BccAddressJSON } } $mailBody = $($mailBody | ConvertTo-Json -Depth 4) if ($WriteRawJSONToDisk) { $mailBody | Out-File $event_id_json_file -Force } ## Send email # $ServicePoint = [System.Net.ServicePointManager]::FindServicePoint('https://graph.microsoft.com') SayInfo "Sending Consolidated Alert for $($events.id -join ';')" $null = Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/beta/users/$($From)/sendmail" -Body $mailBody -Headers $GraphAPIHeader -ContentType 'application/json' # $null = $ServicePoint.CloseConnectionGroup('') } catch { SayInfo "Failed to send Alert for $($events.id -join ';') | $($_.Exception.Message)" $errorFlag = $true return $null } } } } #EndRegion Consolidate #Region NoConsolidate else { if ($events.Count -gt 0) { foreach ($event in ($events | Sort-Object Classification -Descending) ) { $ticket_status = ($event.Status.substring(0, 1).toupper() + $event.Status.substring(1) -creplace '[^\p{Ll}\s]', ' $&').Trim(); $mailSubject = "[$($organizationName)] MS365 Service Health Report | $($event.id) | $($event.Service)" $event_id_file = "$outputDir\$($event.ID).html" $event_id_json_file = "$outputDir\$($event.ID).json" $htmlBody = [System.Collections.ArrayList]@() $null = $htmlBody.Add("<html><head><title>$($mailSubject)</title>") $null = $htmlBody.Add('<style type="text/css">') $null = $htmlBody.Add($css_string) $null = $htmlBody.Add("</style>") $null = $htmlBody.Add("</head><body>") $null = $htmlBody.Add("<hr>") $null = $htmlBody.Add('<table id="section"><tr><th>' + $event.ID + ' | ' + $event.Service + ' | ' + $event.Title + '</th></tr></table>') $null = $htmlBody.Add("<hr>") $null = $htmlBody.Add('<table id="data">') $null = $htmlBody.Add('<tr><th>Status</th><td><b>' + $ticket_status + '</b></td></tr>') $null = $htmlBody.Add('<tr><th>Organization</th><td>' + $organizationName + '</td></tr>') $null = $htmlBody.Add('<tr><th>Classification</th><td>' + $($event.Classification.substring(0, 1).toupper() + $event.Classification.substring(1)) + '</td></tr>') $null = $htmlBody.Add('<tr><th>User Impact</th><td>' + $event.ImpactDescription + '</td></tr>') $null = $htmlBody.Add('<tr><th>Last Updated</th><td>' + [datetime]$event.lastModifiedDateTime + '</td></tr>') $null = $htmlBody.Add('<tr><th>Start Time</th><td>' + [datetime]$event.startDateTime + '</td></tr>') $null = $htmlBody.Add('<tr><th>End Time</th><td>' + $( if ($event.endDateTime) { [datetime]$event.endDateTime } else { "" } ) + '</td></tr>') $latestMessage = ($event.posts[-1].description.content) -replace "`n", "<br />" $null = $htmlBody.Add('<tr><th>Latest Message</th><td>' + $latestMessage + '</td></tr>') $null = $htmlBody.Add('</table>') $null = $htmlBody.Add('<p><font size="2" face="Segoe UI Light"><br />') $null = $htmlBody.Add('<br />') $null = $htmlBody.Add('<a href="' + $moduleInfo.ProjectURI.ToString() + '" target="_blank">' + $moduleInfo.Name.ToString() + ' v' + $moduleInfo.Version.ToString() + ' </a><br></p>') $null = $htmlBody.Add('</body>') $null = $htmlBody.Add('</html>') $htmlBody = $htmlBody -join "`n" #convert to multiline string # https://4sysops.com/archives/dealing-with-smart-quotes-in-powershell/ # $smartSingleQuotes = '[\u2019\u2018]' # $smartDoubleQuotes = '[\u201C\u201D]' # $htmlBody = $htmlBody -replace $smartSingleQuotes, "'" -replace $smartDoubleQuotes, '"' $htmlBody = ReplaceSmartCharacter $htmlBody # $htmlBody | Out-File -FilePath $env:temp if ($WriteReportToDisk -eq $true) { $htmlBody | Out-File $event_id_file -Force } if ($SendEmail -eq $true) { # Recipients $toAddressJSON = @() $To | ForEach-Object { $toAddressJSON += @{EmailAddress = @{Address = $_ } } } try { #message $mailBody = @{ message = @{ subject = $mailSubject body = @{ contentType = "HTML" content = $htmlBody } toRecipients = @( $ToAddressJSON ) internetMessageHeaders = @( @{ name = "X-Mailer" value = "MS365HealthReport (junecastillote)" } ) } } ## Add CC recipients if specified if ($Cc) { $ccAddressJSON = @() $Cc | ForEach-Object { $ccAddressJSON += @{EmailAddress = @{Address = $_ } } } $mailBody.Message += @{ccRecipients = $ccAddressJSON } } ## Add BCC recipients if specified if ($Bcc) { $BccAddressJSON = @() $Bcc | ForEach-Object { $BccAddressJSON += @{EmailAddress = @{Address = $_ } } } $mailBody.Message += @{BccRecipients = $BccAddressJSON } } $mailBody = $($mailBody | ConvertTo-Json -Depth 4) if ($WriteRawJSONToDisk) { $mailBody | Out-File $event_id_json_file -Force } ## Send email # $ServicePoint = [System.Net.ServicePointManager]::FindServicePoint('https://graph.microsoft.com') SayInfo "Sending Alert for $($event.id)" $null = Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/users/$($From)/sendmail" -Body $mailBody -Headers $GraphAPIHeader -ContentType 'application/json' # $null = $ServicePoint.CloseConnectionGroup('') } catch { SayInfo "Failed to send Alert for $($event.id) | $($_.Exception.Message)" $errorFlag = $true return $null } } } } } #EndRegion NoConsolidate #EndRegion Create Report if ($errorFlag) { SayInfo "Setting last run time (NotOK) in $($runHistoryFile) to $now" "$("{0:yyyy-MM-dd H:mm}" -f $now),NotOK" | Add-Content -Path $RunHistoryFile -Force -Confirm:$false } else { SayInfo "Setting last run time (OK) in $($runHistoryFile) to $now" "$("{0:yyyy-MM-dd H:mm}" -f $now),OK" | Add-Content -Path $RunHistoryFile -Force -Confirm:$false } } |