Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FreeBusyChecker Final Version #2264

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions Diagnostics/FreeBusyChecker/FreeBusyChecker.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
<#
.SYNOPSIS

.\FreeBusyChecker.ps1

.DESCRIPTION

This script can be used to validate the Availability configuration of the following Exchange Server Versions:

- Exchange Server
- Exchange Online

Required Permissions:

- Organization Management
- Domain Admin

Please make sure that the account used is a member of the Local Administrator group. This should be fulfilled on Exchange Servers by being a member of the Organization Management group. However, if the group membership was adjusted, or in case the script is executed on a non-Exchange system like a management Server, you need to add your account to the Local Administrator group.

How To Run:

This script must be run as Administrator in Exchange Management Shell on an Exchange Server. You can provide no parameters, and the script will just run against Exchange On-Premises and Exchange Online to query for OAuth and DAuth configuration settings. It will compare existing values with standard values and provide details of what may not be correct.
Please take note that though this script may output that a specific setting is not a standard setting, it does not mean that your configurations are incorrect. For example, DNS may be configured with specific mappings that this script cannot evaluate.

To collect information for Exchange Online a connection to Exchange Online must be established before running the script using Connection Prefix "EO".

Example:

PS C:\scripts\FreeBusyChecker> Connect-ExchangeOnline -Prefix EO

.PARAMETER Auth
Allows you to choose the authentication type to validate.
.PARAMETER Org
Allows you to choose the organization type to validate.
.PARAMETER OnPremUser
Specifies the Exchange On Premise User that will be used to test Free Busy Settings.
.PARAMETER OnlineUser
Specifies the Exchange Online User that will be used to test Free Busy Settings.
.PARAMETER OnPremDomain
Specifies the domain for on-premises Organization.
.PARAMETER OnPremEWSUrl
Specifies the EWS (Exchange Web Services) URL for on-premises Exchange Server.
.PARAMETER OnPremLocalDomain
Specifies the local AD domain for the on-premises Organization.
.PARAMETER Help
Show help for this script.

.EXAMPLE
.\FreeBusyChecker.ps1
This cmdlet will run the Free Busy Checker script and Check Availability OAuth and DAuth Configurations both for Exchange On-Premises and Exchange Online.
.EXAMPLE
.\FreeBusyChecker.ps1 -Auth OAuth
This cmdlet will run the Free Busy Checker Script against OAuth Availability Configurations.
.EXAMPLE
.\FreeBusyChecker.ps1 -Auth DAuth
This cmdlet will run the Free Busy Checker Script against DAuth Availability Configurations.
.EXAMPLE
.\FreeBusyChecker.ps1 -Org ExchangeOnline
This cmdlet will run the Free Busy Checker Script for Exchange Online Availability Configurations.
.EXAMPLE
.\FreeBusyChecker.ps1 -Org ExchangeOnPremise
This cmdlet will run the Free Busy Checker Script for Exchange On-Premises OAuth or DAuth Availability Configurations.
.EXAMPLE
.\FreeBusyChecker.ps1 -Org All
This cmdlet will run the Free Busy Checker Script for Exchange On-Premises and Exchange Online OAuth or DAuth Availability Configurations.
.EXAMPLE
.\FreeBusyChecker.ps1 -Org ExchangeOnPremise -Auth OAuth
This cmdlet will run the Free Busy Checker Script for Exchange On-Premises Availability OAuth Configurations
#>

# Exchange On-Premises
#>
#region Properties and Parameters

#Requires -Module ExchangeOnlineManagement
#Requires -Module ActiveDirectory

[CmdletBinding(DefaultParameterSetName = "FreeBusyInfo_OP", SupportsShouldProcess)]

param(
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[ValidateSet('DAuth', 'OAuth', 'All', '')]
[string[]]$Auth,
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[ValidateSet('ExchangeOnPremise', 'ExchangeOnline')]
[string[]]$Org,
[Parameter(Mandatory = $true, ParameterSetName = "Help")]
[switch]$Help,
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[string]$OnPremisesUser,
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[string]$OnlineUser,
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[string]$OnPremDomain,
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[string]$OnPremEWSUrl,
[Parameter(Mandatory = $false, ParameterSetName = "Test")]
[string]$OnPremLocalDomain,
[Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly", HelpMessage = "Update only script.")]
[switch]$ScriptUpdateOnly,
[Parameter(Mandatory = $false, ParameterSetName = "SkipVersionCheck", HelpMessage = "Skip version check.")]
[switch]$SkipVersionCheck
)
begin {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add GenericScriptUpdate.ps1 here? This adds update capabilities to the script and allows the script to perform an auto-update whenever a new version is released.

Copy link
Contributor Author

@MarcoLFrancisco MarcoLFrancisco Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this:

image

The version CSV does not hold information about this script. I suppose this message will display Script Name and version when the csv holds this information:

image

I see implementations with aka url poiting to csv update file. Should I create one?

image

Can't test this., so not sure if I understood correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't required, but you can put one there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i will commit this evening

. $PSScriptRoot\Functions\OnPremDAuthFunctions.ps1
. $PSScriptRoot\Functions\OnPremOAuthFunctions.ps1
. $PSScriptRoot\Functions\ExoDAuthFunctions.ps1
. $PSScriptRoot\Functions\ExoOAuthFunctions.ps1
. $PSScriptRoot\Functions\htmlContent.ps1
. $PSScriptRoot\Functions\hostOutput.ps1
. $PSScriptRoot\Functions\CommonFunctions.ps1
. $PSScriptRoot\..\..\Shared\Confirm-ExchangeShell.ps1
. $PSScriptRoot\..\..\Shared\ScriptUpdateFunctions\GenericScriptUpdate.ps1
} end {
$Script:countOrgRelIssues = (0)
$Script:WebServicesVirtualDirectory = $null
$Script:Server = hostname
$Script:startingDate = (Get-Date -Format yyyyMMdd_HHmmss)
$Script:htmlFile = "$PSScriptRoot\FBCheckerOutput_$($Script:startingDate).html"

loadingParameters
#Parameter input

if (-not $OnlineUser) {
$Script:UserOnline = Get-RemoteMailbox -ResultSize 1 -WarningAction SilentlyContinue
$Script:UserOnline = $Script:UserOnline.RemoteRoutingAddress.SmtpAddress
} else {
$Script:UserOnline = Get-RemoteMailbox $OnlineUser -ResultSize 1 -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
$Script:UserOnline = $Script:UserOnline.RemoteRoutingAddress.SmtpAddress
}

$Script:ExchangeOnlineDomain = ($Script:UserOnline -split "@")[1]

if ($Script:ExchangeOnlineDomain -like "*.mail.onmicrosoft.com") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other cloud? Is the script expected to work on these (e.g., GCC, Gallatin…)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were not considered at start, I had no reference for this clouds and no way to test.
We can include.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is 100% required before we release. It would be nice yes, but I would rather get something out there and us create an issue for this and address it soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay to address this with a later update. We should mention in the documentation that the script works with WW cloud only.

$Script:ExchangeOnlineAltDomain = (($Script:ExchangeOnlineDomain.Split(".")))[0] + ".onmicrosoft.com"
} else {
$Script:ExchangeOnlineAltDomain = (($Script:ExchangeOnlineDomain.Split(".")))[0] + ".mail.onmicrosoft.com"
}
$Script:temp = "*" + $Script:ExchangeOnlineDomain
$Script:UserOnPrem = ""
if (-not $OnPremisesUser) {
$Script:UserOnPrem = Get-mailbox -ResultSize 2 -WarningAction SilentlyContinue -Filter 'EmailAddresses -like $temp -and HiddenFromAddressListsEnabled -eq $false' -ErrorAction SilentlyContinue
if ($Script:UserOnPrem) {
$Script:UserOnPrem = $Script:UserOnPrem[1].PrimarySmtpAddress.Address
}
} else {
$Script:UserOnPrem = Get-mailbox $OnPremisesUser -WarningAction SilentlyContinue -Filter 'EmailAddresses -like $temp -and HiddenFromAddressListsEnabled -eq $false' -ErrorAction SilentlyContinue
$Script:UserOnPrem = $Script:UserOnPrem.PrimarySmtpAddress.Address
}
$Script:ExchangeOnPremDomain = ($Script:UserOnPrem -split "@")[1]

if (-not $OnPremEWSUrl) {
FetchEWSInformation
} else {
FetchEWSInformation
$Script:ExchangeOnPremEWS = ($OnPremEWSUrl)
}

if (-not $OnPremDomain) {
$ADDomain = Get-ADDomain
$Script:ExchangeOnPremLocalDomain = $ADDomain.forest
} else {
$Script:ExchangeOnPremLocalDomain = $OnPremDomain
}

$Script:ExchangeOnPremLocalDomain = $ADDomain.forest
if ([string]::IsNullOrWhitespace($ADDomain)) {
$Script:ExchangeOnPremLocalDomain = $exchangeOnPremDomain
}

if ($ExchangeOnPremDomain) {
$Script:FedInfoEOP = Get-federationInformation -DomainName $ExchangeOnPremDomain -BypassAdditionalDomainValidation -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Select-Object *
}
#endregion

if ($Help) {
PrintDynamicWidthLine
ShowHelp
PrintDynamicWidthLine
exit
}
#region Show Parameters
$Script:IntraOrgCon = Get-IntraOrganizationConnector -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Where-Object { $_.TargetAddressDomains -contains $Script:ExchangeOnlineDomain } | Select-Object Name, TarGetAddressDomains, DiscoveryEndpoint, Enabled
ShowParameters
CheckParameters
if ($Script:IntraOrgCon.enabled -eq $true) {
$Auth = hostOutputIntraOrgConEnabled($Auth)
}
if ($Script:IntraOrgCon.enabled -eq $false) {
hostOutputIntraOrgConNotEnabled
}
# Free busy Lookup methods
PrintDynamicWidthLine
$Script:OrgRel = Get-OrganizationRelationship | Where-Object { ($_.DomainNames -like $Script:ExchangeOnlineDomain) } -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Select-Object Enabled, Identity, DomainNames, FreeBusy*, TarGet*
$Script:EDiscoveryEndpoint = Get-IntraOrganizationConfiguration -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Select-Object OnPremiseDiscoveryEndpoint
$Script:SPDomainsOnprem = Get-SharingPolicy -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Format-List Domains
$Script:SPOnprem = Get-SharingPolicy -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Select-Object *

if ($Org -contains 'ExchangeOnPremise' -or -not $Org) {
#region DAuth Checks
if ($Auth -like "DAuth" -or -not $Auth -or $Auth -like "All") {
Write-Host " Testing DAuth Configuration"
OrgRelCheck -OrgRelParameter $Script:OrgRel
PrintDynamicWidthLine
FedInfoCheck
FedTrustCheck
AutoDVirtualDCheck
PrintDynamicWidthLine
EWSVirtualDirectoryCheck
AvailabilityAddressSpaceCheck
TestFedTrust
TestOrgRel
}
#endregion
#region OAuth Check
if ($Auth -like "OAuth" -or -not $Auth -or $Auth -like "All") {
Write-Host " Testing OAuth Configuration"
IntraOrgConCheck
PrintDynamicWidthLine
AuthServerCheck
PrintDynamicWidthLine
PartnerApplicationCheck
PrintDynamicWidthLine
ApplicationAccountCheck
PrintDynamicWidthLine
ManagementRoleAssignmentCheck
PrintDynamicWidthLine
AuthConfigCheck
PrintDynamicWidthLine
CurrentCertificateThumbprintCheck
PrintDynamicWidthLine
AutoDVirtualDCheckOAuth
PrintDynamicWidthLine
EWSVirtualDirectoryCheckOAuth
PrintDynamicWidthLine
AvailabilityAddressSpaceCheckOAuth
PrintDynamicWidthLine
OAuthConnectivityCheck
PrintDynamicWidthLine
}
#endregion
}
# EXO Part
if ($Org -contains 'ExchangeOnline' -or -not $Org) {
#region ConnectExo
$Exo = Test-ExchangeOnlineConnection
if (-not ($Exo)) {
Write-Host -ForegroundColor Red "`n Please connect to Exchange Online Using the EXO V3 module using EO as connection Prefix to collect Exchange OnLine Free Busy configuration Information."
Write-Host -ForegroundColor Cyan "`n`n Example: PS C:\Connect-ExchangeOnline -Prefix EO"
Write-Host -ForegroundColor Yellow "`n More Info at:https://learn.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps"
exit
}
Write-Host " Connected to Exchange Online."
$Script:ExoOrgRel = Get-EOOrganizationRelationship | Where-Object { ($_.DomainNames -like $ExchangeOnPremDomain ) } | Select-Object Enabled, Identity, DomainNames, FreeBusy*, TarGet*
$Script:ExoIntraOrgCon = Get-EOIntraOrganizationConnector | Select-Object Name, TarGetAddressDomains, DiscoveryEndpoint, Enabled
$Script:tarGetAddressPr1 = ("https://AutoDiscover." + $ExchangeOnPremDomain + "/AutoDiscover/AutoDiscover.svc/WSSecurity")
$Script:tarGetAddressPr2 = ("https://" + $ExchangeOnPremDomain + "/AutoDiscover/AutoDiscover.svc/WSSecurity")
exoHeaderHtml

#endregion

#region ExoDAuthCheck
if ($Auth -like "DAuth" -or -not $Auth -or $Auth -like "All") {
PrintDynamicWidthLine
Write-Host $TestingExoDAuthConfiguration
ExoOrgRelCheck
PrintDynamicWidthLine
ExoFedOrgIdCheck
PrintDynamicWidthLine
ExoTestOrgRelCheck
SharingPolicyCheck
}
#endregion

#region ExoOauthCheck
if ($Auth -like "OAuth" -or -not $Auth -or $Auth -like "All") {
Write-Host $TestingExoOAuthConfiguration
ExoIntraOrgConCheck
PrintDynamicWidthLine
EXOIntraOrgConfigCheck
PrintDynamicWidthLine
EXOAuthServerCheck
PrintDynamicWidthLine
ExoTestOAuthCheck
PrintDynamicWidthLine
}
#endregion

Write-Host -ForegroundColor Green $ThatIsAllForTheExchangeOnlineSide

PrintDynamicWidthLine
}

Stop-Transcript
}
66 changes: 66 additions & 0 deletions Diagnostics/FreeBusyChecker/Functions/CommonFunctions.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
function Test-ExchangeOnlineConnection {
Write-Host -ForegroundColor Green " Checking Exchange Online Configuration"
Write-Host " Testing Connection to Exchange Online with EO Prefix."
try {
$CheckExoMailbox = Get-EOMailbox $Script:UserOnline -ErrorAction Stop
if ($null -ne $CheckExoMailbox) {
return $true
} else {
return $false
}
} catch {
return $false
}
}
function FetchAutoDiscoverInformation {
if (-not $Script:AutoDiscoveryVirtualDirectory -or -not $Script:AutoDiscoveryVirtualDirectoryOAuth) {
$Script:AutoDiscoveryVirtualDirectory = Get-AutoDiscoverVirtualDirectory -Server $Script:Server | Select-Object Identity, Name, ExchangeVersion, *authentication* -ErrorAction SilentlyContinue
$Script:AutoDiscoveryVirtualDirectoryOAuth = $Script:AutoDiscoveryVirtualDirectory
}
}
function FetchEWSInformation {
if (-not $Script:WebServicesVirtualDirectory -or -not $Script:WebServicesVirtualDirectoryOAuth) {
$Script:WebServicesVirtualDirectory = Get-WebServicesVirtualDirectory -Server $Script:Server | Select-Object Identity, Name, ExchangeVersion, *Authentication*, *url -ErrorAction SilentlyContinue
Copy link
Contributor

@lusassl-msft lusassl-msft Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Get- calls to query the virtual directory information are very time consuming as we query IIS metabase. Can we check if the required information are available in AD tool (by using -ADPropertiesOnly switch parameter). This would improve the execution time of the script. Not all information are stored in AD but we should at least confirm for the use-case of the script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, great point! Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if you NEED to have the correct authentication values, then you can't include -ADPropertiesOnly as that doesn't have the information that you need.

image

Copy link
Contributor Author

@MarcoLFrancisco MarcoLFrancisco Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-ADPropertiesOnly indeed can't be used

Get-WebServicesVirtualDirectory was called multiple times.

It is called initially as it is a required Initial Parameter. It was also called on DAuth and Oauth Checks. It was executed 3 times if user inputs -Auth All when calling script:

image

image

Now it is called only once initially:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, run the cmdlet once and then store it in a script variable or get it with a cmdlet that has it stored within a script variable.

$Script:WebServicesVirtualDirectoryOAuth = $Script:WebServicesVirtualDirectory
$Script:ExchangeOnPremEWS = ($Script:WebServicesVirtualDirectory.externalURL.AbsoluteUri)
}
}
function CheckIfExchangeServer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using Confirm-ExchangeShell here (shared function).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added:

image

image

$exchangeShell = Confirm-ExchangeShell
if (-not($exchangeShell.ShellLoaded)) {
Write-Host "$Server is not an Exchange Server. This script should be run in Exchange Server Management Shell"
exit
}
}
function CheckParameters {
$MissingParameters = @()
if ([string]::IsNullOrWhiteSpace($Script:ExchangeOnlineDomain)) {
$MissingParameters += "Exchange Online Domain. Example: contoso.mail.onmicrosoft.com"
}
if ([string]::IsNullOrWhiteSpace($Script:ExchangeOnPremLocalDomain)) {
$MissingParameters += "Exchange On Premises Local Domain. Example: . 'C:\scripts\FreeBusyChecker\FreeBusyChecker.ps1' -OnPremisesUser [email protected]"
Copy link
Contributor

@lusassl-msft lusassl-msft Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to do something like this for the example as this will reflect the name of the script (even if it was renamed for whatever reason):

.\$($script:MyInvocation.MyCommand.Name) -OnPremisesUser [email protected]

Same for the following examples (line 51, 54 and 57)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will $($script:MyInvocation.MyCommand.Name) return the script name here as we are in a different function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I remember it should return the name of the script even if $($script:MyInvocation.MyCommand.Name) is part of a function / method within the main script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected:

image

}
if ([string]::IsNullOrWhiteSpace($exchangeOnPremDomain)) {
$MissingParameters += "Exchange On Premises Domain. Example: -OnPremLocalDomain Contoso.local"
}
if ([string]::IsNullOrWhiteSpace($exchangeOnPremEWS)) {
$MissingParameters += "Exchange On Premises EWS Virtual Directory External URL. Example: 'C:\FreeBusyChecker.ps1' -OnPremEWSUrl https://mail.contoso.com/EWS/Exchange.asmx"
}
if ([string]::IsNullOrWhiteSpace($Script:UserOnPrem)) {
$MissingParameters += "On Premises User Mailbox. Example: 'C:\FreeBusyChecker.ps1' -OnPremisesUser [email protected]"
}
if ([string]::IsNullOrWhiteSpace($Script:UserOnline)) {
$MissingParameters += "Exchange Online Mailbox. Example: 'C:\FreeBusyChecker.ps1' -OnlineUser [email protected]"
}

if ($MissingParameters.Count -gt 0) {
foreach ($param in $MissingParameters) {
Write-Host -ForegroundColor Red "Please provide a value for $param."
}
exit 1
}
Write-Host -ForegroundColor Cyan "`n All parameters are valid."
return
}
Loading