PSWinDocumentation.O365HealthService.psm1
function Connect-Graph { [cmdletBinding(DefaultParameterSetName = 'ClearText')] param( [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientID')] $ApplicationID, [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientSecret')] $ApplicationKey, [parameter(Mandatory, ParameterSetName = 'Encrypted')][string][alias('ClientSecretEncrypted')] $ApplicationKeyEncrypted, [parameter(Mandatory, ParameterSetName = 'Credential')][PSCredential] $Credential, [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(Mandatory, ParameterSetName = 'ClearText')] [parameter(Mandatory, ParameterSetName = 'Credential')] [string] $TenantDomain, [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(ParameterSetName = 'ClearText')] [parameter(ParameterSetName = 'Credential')] [ValidateSet("https://manage.office.com", "https://graph.microsoft.com", "https://graph.microsoft.com/beta", 'https://graph.microsoft.com/.default')] $Resource = 'https://graph.microsoft.com/.default', [int] $ExpiresTimeout = 30, [switch] $ForceRefesh ) # Comparison V1/V2 https://nicolgit.github.io/AzureAD-Endopoint-V1-vs-V2-comparison/ if (-not $Script:AuthorizationCache) { $Script:AuthorizationCache = [ordered] @{} } if ($Credential) { $RestSplat = @{ ErrorAction = 'Stop' Method = 'POST' Body = @{ grant_type = "client_credentials" client_id = $Credential.UserName client_secret = $Credential.GetNetworkCredential().Password } } } elseif ($ApplicationKey -or $ApplicationKeyEncrypted) { if ($ApplicationKeyEncrypted) { try { $ApplicationKeyTemp = $ApplicationKeyEncrypted | ConvertTo-SecureString -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-Graph - Error: $ErrorMessage" return } $ApplicationKey = [System.Net.NetworkCredential]::new([string]::Empty, $ApplicationKeyTemp).Password } $RestSplat = @{ ErrorAction = 'Stop' Method = 'POST' Body = @{ grant_type = "client_credentials" client_id = $ApplicationID client_secret = $ApplicationKey } } } if ($Script:AuthorizationCache[$ApplicationID] -and -not $ForceRefesh) { if ($Script:AuthorizationCache[$ApplicationID].ExpiresOn -gt [datetime]::UtcNow) { Write-Verbose "Connect-Graph - Using cache for $ApplicationID" return $Script:AuthorizationCache[$ApplicationID] } } if ($Resource -in 'https://graph.microsoft.com/.default', "https://graph.microsoft.com/beta") { # V2 Endpoint $RestSplat['Body']['scope'] = $Resource $RestSplat['Uri'] = "https://login.microsoftonline.com/$($TenantDomain)/oauth2/v2.0/token" } else { # V1 Endpoint $RestSplat['Body']['resource'] = $Resource $RestSplat['Uri'] = "https://login.microsoftonline.com/$($TenantDomain)/oauth2/token" } Write-Verbose "Connect-Graph - EndPoint $($RestSplat['Uri'])" try { $Authorization = Invoke-RestMethod @RestSplat $Key = [ordered] @{ 'Authorization' = "$($Authorization.token_type) $($Authorization.access_token)" 'Extended' = $Authorization 'Error' = '' 'ExpiresOn' = ([datetime]::UtcNow).AddSeconds($Authorization.expires_in - $ExpiresTimeout) 'Splat' = [ordered] @{ ApplicationID = $RestSplat['Body']['client_id'] ApplicationKey = $RestSplat['Body']['client_secret'] TenantDomain = $TenantDomain Resource = $Resource } } $Script:AuthorizationCache[$ApplicationID] = $Key } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Connect-Graph - Error: $ErrorMessage" $Key = [ordered] @{ 'Authorization' = $Null 'Extended' = $Null 'Error' = $ErrorMessage 'ExpiresOn' = $null 'Splat' = [ordered] @{ ApplicationID = $RestSplat['Body']['client_id'] ApplicationKey = $RestSplat['Body']['client_secret'] TenantDomain = $TenantDomain Resource = $Resource } } } $Key } function Convert-TimeToDays { [CmdletBinding()] param ($StartTime, $EndTime, [string] $Ignore = '*1601*') if ($null -ne $StartTime -and $null -ne $EndTime) { try { if ($StartTime -notlike $Ignore -and $EndTime -notlike $Ignore) { $Days = (New-TimeSpan -Start $StartTime -End $EndTime).Days } } catch {} } elseif ($null -ne $EndTime) { if ($StartTime -notlike $Ignore -and $EndTime -notlike $Ignore) { $Days = (New-TimeSpan -Start (Get-Date) -End ($EndTime)).Days } } elseif ($null -ne $StartTime) { if ($StartTime -notlike $Ignore -and $EndTime -notlike $Ignore) { $Days = (New-TimeSpan -Start $StartTime -End (Get-Date)).Days } } return $Days } function Find-TypesNeeded { [CmdletBinding()] param ([Array] $TypesRequired, [Array] $TypesNeeded) [bool] $Found = $False foreach ($Type in $TypesNeeded) { if ($TypesRequired -contains $Type) { $Found = $true break } } return $Found } function Get-Types { [CmdletBinding()] param ([Object] $Types) $TypesRequired = foreach ($Type in $Types) { $Type.GetEnumValues() } return $TypesRequired } function Invoke-Graph { [cmdletBinding(SupportsShouldProcess)] param( [alias('PrimaryUri')][uri] $BaseUri = 'https://graph.microsoft.com/v1.0', [uri] $Uri, [parameter(Mandatory)][alias('Authorization')][System.Collections.IDictionary] $Headers, [validateset('GET', 'DELETE', 'POST', 'PATCH', 'PUT')][string] $Method = 'GET', [string] $ContentType = "application/json; charset=UTF-8", [System.Collections.IDictionary] $Body, [System.Collections.IDictionary] $QueryParameter, [switch] $FullUri ) if ($Headers.MsalToken) { } else { # This forces a reconnect of session in case it's about to time out. If it's not timeouting a cache value is used if ($Headers.Splat) { $Splat = $Headers.Splat $Headers = Connect-Graph @Splat } } if ($Headers.Error) { Write-Warning "Invoke-Graph - Authorization error. Skipping." return } if ($Headers.MsalToken) { $RestSplat = @{ Headers = @{ Authorization = $Headers.MsalToken.TokenType + ' ' + $Headers.MsalToken.AccessToken } Method = $Method ContentType = $ContentType } } else { $RestSplat = @{ Headers = $Headers Method = $Method ContentType = $ContentType } } if ($Body) { $RestSplat['Body'] = $Body | ConvertTo-Json -Depth 5 } if ($FullUri) { $RestSplat.Uri = $Uri } else { $RestSplat.Uri = Join-UriQuery -BaseUri $BaseUri -RelativeOrAbsoluteUri $Uri -QueryParameter $QueryParameter } if ($RestSplat['Body']) { $WhatIfInformation = "Invoking [$Method] " + [System.Environment]::NewLine + $RestSplat['Body'] + [System.Environment]::NewLine } else { $WhatIfInformation = "Invoking [$Method] " } try { if ($Method -eq 'GET') { Write-Verbose "Invoke-Graph - $($WhatIfInformation)over URI $($RestSplat.Uri)" $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false if ($OutputQuery.value) { $OutputQuery.value } if ($OutputQuery.'@odata.nextLink') { $RestSplat.Uri = $OutputQuery.'@odata.nextLink' $MoreData = Invoke-Graph @RestSplat -FullUri if ($MoreData) { $MoreData } } } else { Write-Verbose "Invoke-Graph - $($WhatIfInformation)over URI $($RestSplat.Uri)" if ($PSCmdlet.ShouldProcess($($RestSplat.Uri), $WhatIfInformation)) { $OutputQuery = Invoke-RestMethod @RestSplat -Verbose:$false if ($Method -in 'POST') { $OutputQuery } else { return $true } } } } catch { $RestError = $_.ErrorDetails.Message if ($RestError) { try { $ErrorMessage = ConvertFrom-Json -InputObject $RestError # Write-Warning -Message "Invoke-Graph - [$($ErrorMessage.error.code)] $($ErrorMessage.error.message), exception: $($_.Exception.Message)" Write-Warning -Message "Invoke-Graph - Error: $($_.Exception.Message) $($ErrorMessage.error.message)" } catch { Write-Warning -Message "Invoke-Graph - Error: $($_.Exception.Message)" } } else { Write-Warning -Message "Invoke-Graph - Error: $($_.Exception.Message)" } if ($Method -notin 'GET', 'POST') { return $false } } } function Remove-EmptyValue { [alias('Remove-EmptyValues')] [CmdletBinding()] param([alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Start-TimeLog { [CmdletBinding()] param() [System.Diagnostics.Stopwatch]::StartNew() } function Stop-TimeLog { [CmdletBinding()] param ([Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time, [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner', [switch] $Continue) Begin {} Process { if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" } } End { if (-not $Continue) { $Time.Stop() } return $TimeToExecute } } function Join-UriQuery { <# .SYNOPSIS Provides ability to join two Url paths together including advanced querying .DESCRIPTION Provides ability to join two Url paths together including advanced querying which is useful for RestAPI/GraphApi calls .PARAMETER BaseUri Primary Url to merge .PARAMETER RelativeOrAbsoluteUri Additional path to merge with primary url (optional) .PARAMETER QueryParameter Parameters and their values in form of hashtable .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' -QueryParameter @{ page = 1 per_page = 20 search = 'SearchString' } .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz/wp-json/wp/v2/posts' -QueryParameter @{ page = 1 per_page = 20 search = 'SearchString' } .EXAMPLE Join-UriQuery -BaseUri 'https://evotec.xyz' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .NOTES General notes #> [alias('Join-UrlQuery')] [CmdletBinding()] param ([parameter(Mandatory)][uri] $BaseUri, [parameter(Mandatory = $false)][uri] $RelativeOrAbsoluteUri, [Parameter()][System.Collections.IDictionary] $QueryParameter) if ($BaseUri -and $RelativeOrAbsoluteUri) { $Url = Join-Uri -BaseUri $BaseUri -RelativeOrAbsoluteUri $RelativeOrAbsoluteUri } else { $Url = $BaseUri } if ($QueryParameter) { $Collection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($key in $QueryParameter.Keys) { $Collection.Add($key, $QueryParameter.$key) } } $uriRequest = [System.UriBuilder] $Url if ($Collection) { $uriRequest.Query = $Collection.ToString() } return $uriRequest.Uri.AbsoluteUri } function Join-Uri { <# .SYNOPSIS Provides ability to join two Url paths together .DESCRIPTION Provides ability to join two Url paths together .PARAMETER BaseUri Primary Url to merge .PARAMETER RelativeOrAbsoluteUri Additional path to merge with primary url .EXAMPLE Join-Uri 'https://evotec.xyz/' '/wp-json/wp/v2/posts' .EXAMPLE Join-Uri 'https://evotec.xyz/' 'wp-json/wp/v2/posts' .EXAMPLE Join-Uri -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .EXAMPLE Join-Uri -BaseUri 'https://evotec.xyz/test/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' .NOTES General notes #> [alias('Join-Url')] [cmdletBinding()] param([parameter(Mandatory)][uri] $BaseUri, [parameter(Mandatory)][uri] $RelativeOrAbsoluteUri) return ($BaseUri.OriginalString.TrimEnd('/') + "/" + $RelativeOrAbsoluteUri.OriginalString.TrimStart('/')) } function Connect-O365ServiceHealth { [cmdletBinding(DefaultParameterSetName = 'ClearText')] param( [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientID')] $ApplicationID, [parameter(Mandatory, ParameterSetName = 'ClearText')][string][alias('ClientSecret')] $ApplicationKey, [parameter(Mandatory, ParameterSetName = 'Encrypted')][string][alias('ClientSecretEncrypted')] $ApplicationKeyEncrypted, [parameter(Mandatory, ParameterSetName = 'Credential')][PSCredential] $Credential, [parameter(Mandatory, ParameterSetName = 'Encrypted')] [parameter(Mandatory, ParameterSetName = 'ClearText')] [parameter(Mandatory, ParameterSetName = 'Credential')] [string] $TenantDomain ) $connectGraphSplat = @{ ApplicationID = $ApplicationID ApplicationKey = $ApplicationKey ApplicationKeyEncrypted = $ApplicationKeyEncrypted Credential = $Credential TenantDomain = $TenantDomain Resource = 'https://graph.microsoft.com/.default' } Remove-EmptyValue -Hashtable $connectGraphSplat Connect-Graph @connectGraphSplat } function ConvertFrom-UTCTime { [CmdLetbinding()] param( [Object] $Time, [switch] $ToLocalTime ) if ($null -eq $Script:TimeZoneBias) { try { $TimeZoneBias = (Get-TimeZone -ErrorAction Stop).BaseUtcOffset.TotalMinutes } catch { Write-Warning "ConvertFrom-UTCTime - couldn't get timezone. Please report on GitHub." $TimeZoneBias = 0 } } else { $TimeZoneBias = $Script:TimeZoneBias } if ($Time -is [DateTime]) { $ConvertedTime = $Time } else { if ($null -eq $Time -or $Time -eq '') { return } else { $NewTime = $Time -replace ', at', '' -replace 'UTC', '' -replace 'at' -replace '(^.+?,)' try { [DateTime] $ConvertedTime = [DateTime]::Parse($NewTime) } catch { Write-Warning "ConvertFrom-UTCTime - couldn't convert time. Please report on GitHub - $Time. Skipping conversion..." return $Time } } } if ($ToLocal) { $ConvertedTime.AddMinutes($TimeZoneBias) } else { $ConvertedTime } } function Get-Office365ServiceHealthCurrentStatus { [CmdLetbinding()] param( [System.Collections.IDictionary] $Authorization, [string] $TenantDomain, [switch] $ToLocalTime ) try { $CurrentStatus = Invoke-Graph -Uri "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews?`$expand=issues" -Method GET -Headers $Authorization -FullUri } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Get-Office365ServiceHealthCurrentStatus - Error: $ErrorMessage" return } $Output = @{ } $Output.Simple = foreach ($Status in $CurrentStatus) { [PSCustomObject][ordered] @{ ID = $Status.ID Service = $Status.Service ServiceStatus = $Status.Status StatusTime = $Script:Today Incidents = ($Status.issues | Where-Object { $_.IsResolved -eq $false }).id } } $Output.Extended = foreach ($Status in $CurrentStatus) { [PSCustomObject][ordered] @{ ID = $Status.ID Service = $Status.Service ServiceStatus = $Status.Status StatusTime = $Script:Today Incidents = ($Status.issues | Where-Object { $_.IsResolved -eq $false }).id FeaturesAffected = ($Status.issues | Where-Object { $_.IsResolved -eq $false }).feature FeaturesAffectedGroup = ($Status.issues | Where-Object { $_.IsResolved -eq $false }).featureGroup } } return $Output } function Get-Office365ServiceHealthIssues { [CmdLetbinding()] param( [System.Collections.IDictionary] $Authorization ) try { $AllMessages = Invoke-Graph -Uri "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/issues" -Method GET -Headers $Authorization -FullUri } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Get-Office365ServiceHealthIssues - Error: $ErrorMessage" return } $Output = @{ } $Output.Incidents = foreach ($Message in $AllMessages) { [PSCustomObject] @{ Id = $Message.Id Title = $Message.Title Impact = $Message.impactDescription IsResolved = $Message.IsResolved # HighImpact = $Message.highImpact Classification = $Message.classification Origin = $Message.origin Service = $Message.service LastUpdatedTime = $Message.lastModifiedDateTime LastUpdatedDays = Convert-TimeToDays -StartTime $Message.lastModifiedDateTime -EndTime $Script:Today Status = $Message.status Feature = $Message.feature FeatureGroup = $Message.featureGroup NotifyInApp = ($Message.details | Where-Object { $_.Name -eq 'NotifyInApp' }).Value -eq $true UpdatesCount = $Message.posts.count } } $Output.IncidentsExtended = foreach ($Message in $AllMessages) { [PSCustomObject] @{ Id = $Message.Id Title = $Message.Title Impact = $Message.impactDescription IsResolved = $Message.IsResolved # HighImpact = $Message.highImpact Classification = $Message.classification Origin = $Message.origin Service = $Message.service LastUpdatedTime = $Message.lastModifiedDateTime LastUpdatedDays = Convert-TimeToDays -StartTime $Message.lastModifiedDateTime -EndTime $Script:Today Status = $Message.status Feature = $Message.feature FeatureGroup = $Message.featureGroup NotifyInApp = ($Message.details | Where-Object { $_.Name -eq 'NotifyInApp' }).Value -eq $true UpdatesCount = $Message.posts.count Updates = $Message.Posts | ForEach-Object { $Object = [ordered] @{} foreach ($SubMessage in $_.description.content.Split("`n")) { if ($SubMessage -like 'Title: *') { $Object.Title = $SubMessage -replace 'Title: ', '' } elseif ($SubMessage -like 'User Impact: *') { $Object.UserImpact = $SubMessage -replace 'User Impact: ', '' } elseif ($SubMessage -like 'More info: *') { $Object.MoreInfo = $SubMessage -replace 'More info: ', '' } elseif ($SubMessage -like 'Current status: *') { $Object.CurrentStatus = $SubMessage -replace 'Current status: ', '' } elseif ($SubMessage -like 'Scope of impact: *') { $Object.ScopeOfImpact = $SubMessage -replace 'Scope of impact: ', '' } elseif ($SubMessage -like 'Start time: *') { $Time = $SubMessage -replace 'Start time: ', '' $Object.StartTime = ConvertFrom-UTCTime -Time $Time -ToLocalTime:$ToLocalTime } elseif ($SubMessage -like 'Preliminary root cause: *') { $Object.PreliminaryRootCause = $SubMessage -replace 'Preliminary root cause: ', '' } elseif ($SubMessage -like 'Root cause: *') { $Object.RootCause = $SubMessage -replace 'Root cause: ', '' } elseif ($SubMessage -like 'Next update by: *') { $Time = ($SubMessage -replace 'Next update by: ', '').Trim() $Object.NextUpdateBy = ConvertFrom-UTCTime -Time $Time -ToLocalTime:$ToLocalTime } elseif ($SubMessage -like 'Final status: *') { $Object.FinalStatus = ($SubMessage -replace 'Final status: ', '').Trim() } else { $Object.Other = $SubMessage.Trim() } } [PSCustomObject] @{ Id = $Message.Id Service = $Message.service Created = $_.createdDateTime Type = $_.postType Title = $Object.Title UserImpact = $Object.UserImpact MoreInfo = $Object.MoreInfo CurrentStatus = $Object.CurrentStatus ScopeOfImpact = $Object.ScopeOfImpact StartTime = $Object.StartTime PreliminaryRootCause = $Object.PreliminaryRootCause RootCause = $Object.RootCause FinalStatus = $Object.FinalStatus NextUpdateBy = $Object.NextUpdateBy Other = $Object.Other } } } } $Output.IncidentsUpdates = foreach ($Message in $Output.IncidentsExtended) { foreach ($Post in $Message.Updates) { $Post } } $Output } function Get-Office365ServiceHealthMessages { [CmdLetbinding()] param( [System.Collections.IDictionary] $Authorization ) try { $AllMessages = Invoke-Graph -Uri "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages" -Method GET -Headers $Authorization -FullUri } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Get-Office365ServiceHealthMessages - Error: $ErrorMessage" return } $Output = @{ } $Output.MessageCenterInformation = foreach ($Message in $AllMessages) { $ActionRequiredDays = Convert-TimeToDays -StartTime $Message.actionRequiredByDateTime -EndTime $Script:Today [PSCustomObject] @{ Id = $Message.Id Title = $Message.Title Service = $Message.services LastUpdatedTime = $Message.lastModifiedDateTime LastUpdatedDays = Convert-TimeToDays -StartTime $Message.lastModifiedDateTime -EndTime $Script:Today ActionRequiredByDateTime = $Message.actionRequiredByDateTime ActionRequiredDays = if ($ActionRequiredDays -eq 0) { $null } else { - $ActionRequiredDays } Tags = $Message.Tags RoadmapId = ($Message.details | Where-Object { $_.name -eq 'roadmapids' }).Value Category = $Message.category } } $Output.MessageCenterInformationExtended = foreach ($Message in $AllMessages) { $ActionRequiredDays = Convert-TimeToDays -StartTime $Message.actionRequiredByDateTime -EndTime $Script:Today [PSCustomObject] @{ Id = $Message.Id Title = $Message.Title Service = $Message.services LastUpdatedTime = $Message.lastModifiedDateTime LastUpdatedDays = Convert-TimeToDays -StartTime $Message.lastModifiedDateTime -EndTime $Script:Today ActionRequiredByDateTime = $Message.actionRequiredByDateTime ActionRequiredDays = if ($ActionRequiredDays -eq 0) { $null } else { - $ActionRequiredDays } Tags = $Message.Tags Bloglink = ($Message.details | Where-Object { $_.name -eq 'bloglink' }).Value RoadmapId = ($Message.details | Where-Object { $_.name -eq 'roadmapids' }).Value RoadmapIdLinks = ($Message.details | Where-Object { $_.name -eq 'roadmapids' }).Value | ForEach-Object { "https://www.microsoft.com/en-us/microsoft-365/roadmap?filters=&searchterms=$_" } Category = $Message.category IsMajorChange = $Message.isMajorChange Severity = $Message.Severity StartTime = $Message.startDateTime EndTime = $Message.endDateTime Message = $Message.body.content } } $Output } function Get-Office365ServiceHealthServices { [CmdLetbinding()] param( [System.Collections.IDictionary] $Authorization, [string] $TenantDomain ) try { $Services = Invoke-Graph -Uri "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews" -Method GET -Headers $Authorization -FullUri } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Warning -Message "Get-Office365ServiceHealthServices - Error: $ErrorMessage" return } $Output = @{ } $Output.Simple = foreach ($Service in $Services) { [PSCustomObject][ordered] @{ ID = $Service.ID Service = $Service.service } } return $Output } function Get-Office365Health { [CmdLetbinding()] param( [string][alias('ClientID')] $ApplicationID, [string][alias('ClientSecret')] $ApplicationKey, [string] $TenantDomain, [PSWinDocumentation.Office365Health[]] $TypesRequired = [PSWinDocumentation.Office365Health]::All ) $StartTime = Start-TimeLog $Script:TimeZoneBias = (Get-CimInstance -ClassName Win32_TimeZone -Verbose:$false).Bias $Script:Today = Get-Date if ($null -eq $TypesRequired -or $TypesRequired -contains [PSWinDocumentation.Office365Health]::All) { $TypesRequired = Get-Types -Types ([PSWinDocumentation.Office365Health]) } $Authorization = Connect-O365ServiceHealth -ApplicationID $ApplicationID -ApplicationKey $ApplicationKey -TenantDomain $TenantDomain if ($null -eq $Authorization) { return } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::Services)) { $Services = Get-Office365ServiceHealthServices -Authorization $Authorization -TenantDomain $TenantDomain } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @( [PSWinDocumentation.Office365Health]::CurrentStatus, [PSWinDocumentation.Office365Health]::CurrentStatusExtended )) { $CurrentStatus = Get-Office365ServiceHealthCurrentStatus -Authorization $Authorization -TenantDomain $TenantDomain -ToLocalTime:$ToLocalTime } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @( [PSWinDocumentation.Office365Health]::Incidents, [PSWinDocumentation.Office365Health]::IncidentsExtended [PSWinDocumentation.Office365Health]::IncidentsUpdates )) { $Issues = Get-Office365ServiceHealthIssues -Authorization $Authorization } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @( [PSWinDocumentation.Office365Health]::MessageCenterInformation, [PSWinDocumentation.Office365Health]::MessageCenterInformationExtended )) { $Messages = Get-Office365ServiceHealthMessages -Authorization $Authorization } $Output = [ordered] @{} if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::Services)) { $Output.Services = $Services.Simple } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::CurrentStatus)) { $Output.CurrentStatus = $CurrentStatus.Simple } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::CurrentStatusExtended)) { $Output.CurrentStatusExtended = $CurrentStatus.Extended } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::MessageCenterInformation)) { $Output.MessageCenterInformation = $Messages.MessageCenterInformation | Sort-Object -Property LastUpdatedTime -Descending } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::MessageCenterInformationExtended)) { $Output.MessageCenterInformationExtended = $Messages.MessageCenterInformationExtended | Sort-Object -Property LastUpdatedTime -Descending } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::Incidents)) { $Output.Incidents = $Issues.Incidents | Sort-Object -Property LastUpdatedTime -Descending } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::IncidentsExtended)) { $Output.IncidentsExtended = $Issues.IncidentsExtended | Sort-Object -Property LastUpdatedTime -Descending } if (Find-TypesNeeded -TypesRequired $TypesRequired -TypesNeeded @([PSWinDocumentation.Office365Health]::IncidentsUpdates)) { $Output.IncidentsUpdates = $Issues.IncidentsUpdates | Sort-Object -Property LastUpdatedTime -Descending } $EndTime = Stop-TimeLog -Time $StartTime -Option OneLiner Write-Verbose "Get-Office365Health - Time to process: $EndTime" return $Output } Add-Type -TypeDefinition @" using System; namespace PSWinDocumentation { [Flags] public enum Office365Health { All, Services, CurrentStatus, CurrentStatusExtended, MessageCenterInformation, MessageCenterInformationExtended, Incidents, IncidentsExtended, IncidentsUpdates } } "@ # Export functions and aliases as required Export-ModuleMember -Function @('Get-Office365Health') -Alias @() # SIG # Begin signature block # MIIdWQYJKoZIhvcNAQcCoIIdSjCCHUYCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUtajoiTHdkqvjdHmWFctG7olf # VHegghhnMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B # AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg # Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg # +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT # XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5 # a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g # 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1 # roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf # GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G # A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL # gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3 # cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr # EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+ # fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q # Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu # 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw # 8jCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAw # MDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4G # A1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUA # A4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4d # yG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnH # bQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1L # sVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpK # jIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756Wwog # L0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNV # HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8v # d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIl # ssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgw # MqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMu # Y3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk # LXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQw # DQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIl # xHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie3 # 8+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTz # ZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0y # OIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4Tr # weOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUwMIIEGKADAgEC # AhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVT # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j # b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEw # MjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV # BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEi # MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7 # RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p # 0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj # 6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grk # V7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHy # DxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMB # AAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAT # BgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCB # gQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNl # cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgG # CmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1 # DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEL # BQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q # 3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/ # kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dc # IFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6 # dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT # +hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUxMIIEGaADAgECAhAKoSXW1jIbfkHk # Bdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE # aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMT # G0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcxMjAwMDBaFw0z # MTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0 # IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB # AQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rEDgdFM1EQfdD5 # fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym9jJZ/l9lP+Cb # 6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT7l3ImgtU # 46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM/fDqR9mI # UF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F0IQZchfx # FwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHOMIIByjAd # BgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAUReuir/SS # y4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC # AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUF # BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6 # Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j # cnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwUAYDVR0gBEkw # RzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2lj # ZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4IBAQBxlRLp # UYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/MrHwwhWiq3BTQd # aq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVDBGiy23UC # 4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3CzddWThZN+ # tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UBJrZspe6H # USHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0e/VWMyIv # IjayS6JKldj1po5SMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkq # hkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoX # DTIzMDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tp # ZTERMA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlz # IEVWT1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqD # Bqlnr3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfF # jVye3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub # +3tii0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf # 3tZZzO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6 # Ea41zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQAB # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O # BBYEFBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1s # z4lsLARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsR # XPHUF/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHI # NrTCvPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8 # Rj9yG4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6 # o6ESJre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNp # ezQug9ufqExx6lHYDjGCBFwwggRYAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv # BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC # EATV3B9I6snYUgC6zZqbKqcwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI # oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB # CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFIv6SiU1EsrEHdmU9uE5 # vvC0vHfzMA0GCSqGSIb3DQEBAQUABIIBABB9tVDCOpUV0Dt1QxbsVFFRCzRul28N # GanlwCr7NM9cZ2MHt/Zcq+mtR+8Jw9OfaM6Ozw4Ie8FD3NhV+LhnRlYfJseeABXh # WSIRMV4gUA2ztbHYvzFbFRimzd/fl64cEhs8TnmGhjwsRQTTZXh7YvgacZ5Kjw4d # dOnySBzF96oyjdI7F2U1oSMSaXGu48MbA9tilJs+rGoCnIm/KrtsFF6uLM9OYaSb # FI735WWSilbISN1orG4YNXMLElOQUJE6XBG0pOUfWRGKfPUJMU4WaSXYq36hLAKo # YdVPnFsiJQ6q0rFClJxK1WD2dpo40QDK9Cn62pokdpkjKfFO5y7+UeOhggIwMIIC # LAYJKoZIhvcNAQkGMYICHTCCAhkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G # A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQQIQ # DUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzEL # BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIyMDEzMTEzMTExMlowLwYJKoZI # hvcNAQkEMSIEICMdKY1tPqJ9OcyCBRM0+r00TXegdAWNjRYzrdl0Rxz9MA0GCSqG # SIb3DQEBAQUABIIBACSMGYDSJUJBcdkDwhL3/uCrjUQbDXdDhUEpJrSLe4eVcHC9 # BX88GEiiYH7ZOG0TQ2/58DqGKt3kiukD9u2gky7goiC46NUvRJfjlJa+LC6e4z12 # OAsdxpgbFmid1XMtpP1JfRdfWsleFR64oNL5jHsDS0ajbR4bUia7Nev6ejiCcQR4 # oR1so2mDzMPUzyeZszFZ/pYpw2v7nbIT1WSimi7edbtLYooT5E1oYPTsOvod6/Lj # Cu8FOxKWY5ZrMfnbIPGDjxxcw2F+VC0R5w7Y+7/NyYnzLlhmSW/M2skYEsxV3/ex # /Sz5PPlIBDr5eHqFo6F0sW7baa5fEayVMJZi9kA= # SIG # End signature block |