diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt
index 5b9a2bff5f..8bb4f8b2dc 100644
--- a/.build/cspell-words.txt
+++ b/.build/cspell-words.txt
@@ -151,12 +151,15 @@ unconfigured
unrequired
USERDNSDOMAIN
Vianet
+Visio
vmxnet
vssadmin
vsstester
+Vssx
vuln
wbxml
Webex
Weve
wevtutil
windir
+Xlsb
diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1
index 772f22bb6c..b8426c1ec1 100644
--- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1
@@ -3,7 +3,7 @@
. $PSScriptRoot\Add-AnalyzedResultInformation.ps1
. $PSScriptRoot\Get-DisplayResultsGroupingKey.ps1
-. $PSScriptRoot\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerFrequentConfigurationIssues {
[CmdletBinding()]
param(
diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerKnownBuildIssues.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerKnownBuildIssues.ps1
index fe9de3db6f..415637e24f 100644
--- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerKnownBuildIssues.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerKnownBuildIssues.ps1
@@ -232,6 +232,20 @@ function Invoke-AnalyzerKnownBuildIssues {
InformationUrl = (GetKnownIssueInformation @infoParams)
}
TestForKnownBuildIssues @params
+
+ Write-Verbose "Work on March 2024 Security Updates"
+ $infoParams = @{
+ Name = "Known Issues with Mar 2024 Security Updates"
+ Url = "https://support.microsoft.com/help/5037171"
+ }
+ $params = @{
+ CurrentVersion = $currentVersion
+ KnownBuildIssuesToFixes = @((GetKnownIssueBuildInformation "15.2.1544.9" $null),
+ (GetKnownIssueBuildInformation "15.2.1258.32" $null),
+ (GetKnownIssueBuildInformation "15.1.2507.37", $null))
+ InformationUrl = (GetKnownIssueInformation @infoParams)
+ }
+ TestForKnownBuildIssues @params
} catch {
Write-Verbose "Failed to run TestForKnownBuildIssues"
Invoke-CatchActions
diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1
index 5ee4ae2827..42a856d01e 100644
--- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1
@@ -3,7 +3,7 @@
. $PSScriptRoot\Add-AnalyzedResultInformation.ps1
. $PSScriptRoot\Get-DisplayResultsGroupingKey.ps1
-. $PSScriptRoot\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\Shared\CompareExchangeBuildLevel.ps1
. $PSScriptRoot\..\..\..\Shared\VisualCRedistributableVersionFunctions.ps1
. $PSScriptRoot\..\..\..\Shared\Get-NETFrameworkVersion.ps1
function Invoke-AnalyzerOsInformation {
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Get-SerializedDataSigningState.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Get-SerializedDataSigningState.ps1
index 555d389cdc..bfd18147ec 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Get-SerializedDataSigningState.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Get-SerializedDataSigningState.ps1
@@ -2,7 +2,7 @@
# Licensed under the MIT License.
. $PSScriptRoot\..\Get-FilteredSettingOverrideInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
# Used to determine the state of the Serialized Data Signing on the server.
function Get-SerializedDataSigningState {
[CmdletBinding()]
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityADV24199947.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityADV24199947.ps1
new file mode 100644
index 0000000000..b6f24ef412
--- /dev/null
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityADV24199947.ps1
@@ -0,0 +1,69 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
+
+<#
+.DESCRIPTION
+ Check for ADV24199947 Outside In Module vulnerability
+ Must be on March 2024 SU and no overrides in place to be considered secure.
+ Overrides are found in the Configuration.xml file with appending flag of |NO
+ This only needs to occur on the Mailbox Servers Roles
+#>
+function Invoke-AnalyzerSecurityADV24199947 {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [ref]$AnalyzeResults,
+
+ [Parameter(Mandatory = $true)]
+ [object]$SecurityObject,
+
+ [Parameter(Mandatory = $true)]
+ [object]$DisplayGroupingKey
+ )
+ process {
+ Write-Verbose "Calling: $($MyInvocation.MyCommand)"
+
+ $params = @{
+ AnalyzedInformation = $AnalyzeResults
+ DisplayGroupingKey = $DisplayGroupingKey
+ Name = "Security Vulnerability"
+ DisplayWriteType = "Red"
+ Details = "{0}"
+ DisplayTestingValue = "ADV24199947"
+ }
+
+ if ($SecurityObject.IsEdgeServer) {
+ Write-Verbose "Skipping over test as this is an edge server."
+ return
+ }
+
+ $isVulnerable = (-not (Test-ExchangeBuildGreaterOrEqualThanSecurityPatch -CurrentExchangeBuild $SecurityObject.BuildInformation -SUName "Mar24SU"))
+
+ # if patch is installed, need to check for the override.
+ if ($isVulnerable -eq $false) {
+ Write-Verbose "Mar24SU is installed, checking to see if override is set"
+ # Key for the file content information
+ $key = [System.IO.Path]::Combine($SecurityObject.ExchangeInformation.RegistryValues.FipFsDatabasePath, "Configuration.xml")
+ $unknownError = [string]::IsNullOrEmpty($SecurityObject.ExchangeInformation.RegistryValues.FipFsDatabasePath) -or
+ ($null -eq $SecurityObject.ExchangeInformation.FileContentInformation[$key])
+
+ if ($unknownError) {
+ $params.Details += " Unable to determine if override is set due to no data to review."
+ $params.DisplayWriteType = "Yellow"
+ $isVulnerable = $true
+ } else {
+ $isVulnerable = $null -ne ($SecurityObject.ExchangeInformation.FileContentInformation[$key] | Select-String "\|NO")
+ }
+ }
+
+ if ($isVulnerable) {
+ $params.Details = ("$($params.Details)`r`n`t`tSee: https://portal.msrc.microsoft.com/security-guidance/advisory/{0} for more information." -f "ADV24199947")
+ Add-AnalyzedResultInformation @params
+ } else {
+ Write-Verbose "Not vulnerable to ADV24199947"
+ }
+ }
+}
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityAMSIConfigState.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityAMSIConfigState.ps1
index 94e95c97d4..4402fb9d85 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityAMSIConfigState.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityAMSIConfigState.ps1
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
. $PSScriptRoot\..\Get-FilteredSettingOverrideInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityAMSIConfigState {
[CmdletBinding()]
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-1730.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-1730.ps1
index 5a29b178c4..a83b2b145a 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-1730.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-1730.ps1
@@ -2,7 +2,7 @@
# Licensed under the MIT License.
. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityCve-2021-1730 {
[CmdletBinding()]
param(
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-34470.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-34470.ps1
index 18d55dde70..23d10c34e9 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-34470.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2021-34470.ps1
@@ -2,7 +2,7 @@
# Licensed under the MIT License.
. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityCve-2021-34470 {
[CmdletBinding()]
param(
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1
index 096874f709..632eb8df0f 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1
@@ -2,7 +2,7 @@
# Licensed under the MIT License.
. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityCve-MarchSuSpecial {
[CmdletBinding()]
param(
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCveCheck.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCveCheck.ps1
index 6ca87e5393..c4bb99ed89 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCveCheck.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCveCheck.ps1
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
+. $PSScriptRoot\Invoke-AnalyzerSecurityADV24199947.ps1
. $PSScriptRoot\Invoke-AnalyzerSecurityCve-2020-0796.ps1
. $PSScriptRoot\Invoke-AnalyzerSecurityCve-2020-1147.ps1
. $PSScriptRoot\Invoke-AnalyzerSecurityCve-2021-1730.ps1
@@ -12,7 +13,7 @@
. $PSScriptRoot\Invoke-AnalyzerSecurityExtendedProtectionConfigState.ps1
. $PSScriptRoot\Invoke-AnalyzerSecurityIISModules.ps1
. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityCveCheck {
[CmdletBinding()]
param(
@@ -132,6 +133,7 @@ function Invoke-AnalyzerSecurityCveCheck {
"Aug23SU" = (NewCveEntry @("CVE-2023-38181", "CVE-2023-38182", "CVE-2023-38185", "CVE-2023-35368", "CVE-2023-35388", "CVE-2023-36777", "CVE-2023-36757", "CVE-2023-36756", "CVE-2023-36745", "CVE-2023-36744") @($ex2016, $ex2019))
"Oct23SU" = (NewCveEntry @("CVE-2023-36778") @($ex2016, $ex2019))
"Nov23SU" = (NewCveEntry @("CVE-2023-36050", "CVE-2023-36039", "CVE-2023-36035", "CVE-2023-36439") @($ex2016, $ex2019))
+ "Mar24SU" = (NewCveEntry @("CVE-2024-26198") @($ex2016, $ex2019))
}
# Need to organize the list so oldest CVEs come out first.
@@ -208,6 +210,7 @@ function Invoke-AnalyzerSecurityCveCheck {
Invoke-AnalyzerSecurityCve-2023-36434 -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
Invoke-AnalyzerSecurityCveAddressedBySerializedDataSigning -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
Invoke-AnalyzerSecurityCve-MarchSuSpecial -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
+ Invoke-AnalyzerSecurityADV24199947 -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
# Make sure that these stay as the last one to keep the output more readable
Invoke-AnalyzerSecurityExtendedProtectionConfigState -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
}
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1
index 2bfb62bbc7..81b8bbb03b 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1
@@ -2,7 +2,7 @@
# Licensed under the MIT License.
. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityMitigationService {
[CmdletBinding()]
param(
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityOverrides.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityOverrides.ps1
index 3408181194..b8a75e272f 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityOverrides.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityOverrides.ps1
@@ -3,7 +3,7 @@
. $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1
. $PSScriptRoot\..\Get-FilteredSettingOverrideInformation.ps1
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
function Invoke-AnalyzerSecurityOverrides {
[CmdletBinding()]
param(
diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityVulnerability.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityVulnerability.ps1
index b9189de1bc..e29e6ea06d 100644
--- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityVulnerability.ps1
+++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityVulnerability.ps1
@@ -32,8 +32,10 @@ function Invoke-AnalyzerSecurityVulnerability {
$buildVersion = $HealthServerObject.ExchangeInformation.BuildInformation.VersionInformation
$noLongerSecureExchange = ($buildVersion.ExtendedSupportDate -le ([DateTime]::Now)) -and $buildVersion.LatestSU -eq $false
- if ($null -eq $securityVulnerabilities -and
- ($null -ne $iisModule -or $iisModule.DisplayValue -eq $false) -and
+ if ((($null -eq $securityVulnerabilities -and
+ $HealthServerObject.ExchangeInformation.GetExchangeServer.IsEdgeServer) -or
+ ($null -eq $securityVulnerabilities -and
+ ($null -ne $iisModule -or $iisModule.DisplayValue -eq $false))) -and
(-not $noLongerSecureExchange)) {
$params = $baseParams + @{
Details = "All known security issues in this version of the script passed."
diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeAES256CBCDetails.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeAES256CBCDetails.ps1
index 08270470cd..fbbdd7de2d 100644
--- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeAES256CBCDetails.ps1
+++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeAES256CBCDetails.ps1
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
-. $PSScriptRoot\..\..\Helpers\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1
. $PSScriptRoot\..\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1
. $PSScriptRoot\..\..\..\..\Shared\ErrorMonitorFunctions.ps1
function Get-ExchangeAES256CBCDetails {
diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1
index 795fc6e928..9d5abe474d 100644
--- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1
+++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1
@@ -5,6 +5,7 @@
. $PSScriptRoot\..\..\..\..\Shared\ErrorMonitorFunctions.ps1
. $PSScriptRoot\..\..\..\..\Shared\Get-ExchangeBuildVersionInformation.ps1
. $PSScriptRoot\..\..\..\..\Shared\Get-ExchangeSettingOverride.ps1
+. $PSScriptRoot\..\..\..\..\Shared\Get-ExSetupFileVersionInfo.ps1
. $PSScriptRoot\..\..\..\..\Shared\Get-FileContentInformation.ps1
. $PSScriptRoot\IISInformation\Get-ExchangeAppPoolsInformation.ps1
. $PSScriptRoot\IISInformation\Get-ExchangeServerIISSettings.ps1
@@ -16,7 +17,6 @@
. $PSScriptRoot\Get-ExchangeServerMaintenanceState.ps1
. $PSScriptRoot\Get-ExchangeUpdates.ps1
. $PSScriptRoot\Get-ExchangeVirtualDirectories.ps1
-. $PSScriptRoot\Get-ExSetupDetails.ps1
. $PSScriptRoot\Get-FIPFSScanEngineVersionState.ps1
. $PSScriptRoot\Get-ServerRole.ps1
function Get-ExchangeInformation {
@@ -35,7 +35,7 @@ function Get-ExchangeInformation {
$windows2016OrGreater = Invoke-ScriptBlockHandler @params
$getExchangeServer = (Get-ExchangeServer -Identity $Server -Status)
$exchangeCertificates = Get-ExchangeServerCertificates -Server $Server
- $exSetupDetails = Get-ExSetupDetails -Server $Server
+ $exSetupDetails = Get-ExSetupFileVersionInfo -Server $Server -CatchActionFunction ${Function:Invoke-CatchActions}
if ($null -eq $exSetupDetails) {
# couldn't find ExSetup.exe this should be rare so we are just going to handle this by displaying the AdminDisplayVersion from Get-ExchangeServer
@@ -117,7 +117,23 @@ function Get-ExchangeInformation {
"$([System.IO.Path]::Combine($serverExchangeBinDirectory, "Search\Ceres\Runtime\1.0\noderunner.exe.config"))")
}
- $applicationConfigFileStatus = Get-FileContentInformation @configParams
+ if ($getExchangeServer.IsEdgeServer -eq $false -and
+ (-not ([string]::IsNullOrEmpty($registryValues.FipFsDatabasePath)))) {
+ $configParams.FileLocation += "$([System.IO.Path]::Combine($registryValues.FipFsDatabasePath, "Configuration.xml"))"
+ }
+
+ $getFileContentInformation = Get-FileContentInformation @configParams
+ $applicationConfigFileStatus = @{}
+ $fileContentInformation = @{}
+
+ foreach ($key in $getFileContentInformation.Keys) {
+ if ($key -like "*.exe.config") {
+ $applicationConfigFileStatus.Add($key, $getFileContentInformation[$key])
+ } else {
+ $fileContentInformation.Add($key, $getFileContentInformation[$key])
+ }
+ }
+
$serverMaintenance = Get-ExchangeServerMaintenanceState -Server $Server -ComponentsToSkip "ForwardSyncDaemon", "ProvisioningRps"
$settingOverrides = Get-ExchangeSettingOverride -Server $Server -CatchActionFunction ${Function:Invoke-CatchActions}
@@ -192,6 +208,7 @@ function Get-ExchangeInformation {
SettingOverrides = $settingOverrides
FIPFSUpdateIssue = $FIPFSUpdateIssue
AES256CBCInformation = $aes256CbcDetails
+ FileContentInformation = $fileContentInformation
}
}
}
diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeRegistryValues.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeRegistryValues.ps1
index a10e59c7f8..1caf2912d2 100644
--- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeRegistryValues.ps1
+++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeRegistryValues.ps1
@@ -56,6 +56,12 @@ function Get-ExchangeRegistryValues {
ValueType = "String"
}
+ $fipFsDatabasePathParams = $baseParams + @{
+ SubKey = "SOFTWARE\Microsoft\ExchangeServer\v15\FIP-FS"
+ GetValue = "DatabasePath"
+ ValueType = "String"
+ }
+
return [PSCustomObject]@{
DisableBaseTypeCheckForDeserialization = [int](Get-RemoteRegistryValue @baseTypeCheckForDeserializationParams)
CtsProcessorAffinityPercentage = [int](Get-RemoteRegistryValue @ctsParams)
@@ -65,5 +71,6 @@ function Get-ExchangeRegistryValues {
SerializedDataSigning = [int](Get-RemoteRegistryValue @serializedDataSigningParams)
MsiInstallPath = [string](Get-RemoteRegistryValue @installDirectoryParams)
DisablePreservation = [string](Get-RemoteRegistryValue @disablePreservationParams)
+ FipFsDatabasePath = [string](Get-RemoteRegistryValue @fipFsDatabasePathParams)
}
}
diff --git a/Diagnostics/HealthChecker/Tests/DataCollection/E15/Exchange/Configuration.xml b/Diagnostics/HealthChecker/Tests/DataCollection/E15/Exchange/Configuration.xml
new file mode 100644
index 0000000000..11ce33ef2d
Binary files /dev/null and b/Diagnostics/HealthChecker/Tests/DataCollection/E15/Exchange/Configuration.xml differ
diff --git a/Diagnostics/HealthChecker/Tests/DataCollection/E16/Exchange/Configuration.xml b/Diagnostics/HealthChecker/Tests/DataCollection/E16/Exchange/Configuration.xml
new file mode 100644
index 0000000000..2360c39d93
Binary files /dev/null and b/Diagnostics/HealthChecker/Tests/DataCollection/E16/Exchange/Configuration.xml differ
diff --git a/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/Configuration.xml b/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/Configuration.xml
new file mode 100644
index 0000000000..f52aa9c191
Binary files /dev/null and b/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/Configuration.xml differ
diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1
index b0ffcde76e..96e3367dd9 100644
--- a/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1
+++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1
@@ -141,11 +141,12 @@ Describe "Testing Health Checker by Mock Data Imports - Exchange 2016" {
$cveTests = GetObject "Security Vulnerability"
$cveTests.Contains("CVE-2020-1147") | Should -Be $true
$cveTests.Contains("CVE-2023-36039") | Should -Be $true
- $cveTests.Count | Should -Be 49
+ $cveTests.Contains("ADV24199947") | Should -Be $true
+ $cveTests.Count | Should -Be 51
$downloadDomains = GetObject "CVE-2021-1730"
$downloadDomains.DownloadDomainsEnabled | Should -Be "false"
- $Script:ActiveGrouping.Count | Should -Be 56
+ $Script:ActiveGrouping.Count | Should -Be 58
}
}
diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1
index b926686dcf..29b4722c78 100644
--- a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1
+++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1
@@ -150,7 +150,8 @@ Describe "Testing Health Checker by Mock Data Imports" {
$cveTests.Contains("CVE-2020-1147") | Should -Be $true
$cveTests.Contains("CVE-2023-36434") | Should -Be $true
$cveTests.Contains("CVE-2023-36039") | Should -Be $true
- $cveTests.Count | Should -Be 49
+ $cveTests.Contains("ADV24199947") | Should -Be $true
+ $cveTests.Count | Should -Be 51
$downloadDomains = GetObject "CVE-2021-1730"
$downloadDomains.DownloadDomainsEnabled | Should -Be "False"
TestObjectMatch "Extended Protection Vulnerable" "True" -WriteType "Red"
@@ -173,7 +174,7 @@ Describe "Testing Health Checker by Mock Data Imports" {
-MockWith { return Import-Clixml "$Script:MockDataCollectionRoot\Hardware\Physical_Win32_PhysicalMemory.xml" }
Mock Get-WmiObjectHandler -ParameterFilter { $Class -eq "Win32_Processor" } `
-MockWith { return Import-Clixml "$Script:MockDataCollectionRoot\Hardware\Physical_Win32_Processor.xml" }
- Mock Get-ExSetupDetails { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup1.xml" }
+ Mock Get-ExSetupFileVersionInfo { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup1.xml" }
Mock Get-WebSite -ParameterFilter { $Name -eq "Default Web Site" } -MockWith { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\IIS\GetWebSite_DefaultWebSite1.xml" }
Mock Get-WebConfigFile -ParameterFilter { $PSPath -eq "IIS:\Sites\Default Web Site" } -MockWith { return [PSCustomObject]@{ FullName = "$Script:MockDataCollectionRoot\Exchange\IIS\DefaultWebSite_web2.config" } }
Mock Invoke-ScriptBlockHandler -ParameterFilter { $ScriptBlockDescription -eq "Getting applicationHost.config" } -MockWith { return Get-Content "$Script:MockDataCollectionRoot\Exchange\IIS\applicationHost2.config" -Raw }
diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1
index a36ad08e03..c0fc07e9cd 100644
--- a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1
+++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1
@@ -220,7 +220,7 @@ Describe "Testing Health Checker by Mock Data Imports" {
Mock Get-OwaVirtualDirectory { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetOwaVirtualDirectory2.xml" }
Mock Get-AcceptedDomain { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetAcceptedDomain_Bad.xml" }
Mock Get-DnsClient { return Import-Clixml "$Script:MockDataCollectionRoot\OS\GetDnsClient1.xml" }
- Mock Get-ExSetupDetails { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup1.xml" }
+ Mock Get-ExSetupFileVersionInfo { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup1.xml" }
Mock Invoke-ScriptBlockHandler -ParameterFilter { $ScriptBlockDescription -eq "Getting applicationHost.config" } -MockWith { return Get-Content "$Script:MockDataCollectionRoot\Exchange\IIS\applicationHost1.config" -Raw }
Mock Get-Content -ParameterFilter { $Path -eq "C:\Program Files\Microsoft\Exchange Server\V15\Bin\Search\Ceres\Runtime\1.0\noderunner.exe.config" } -MockWith { Get-Content "$Script:MockDataCollectionRoot\Exchange\noderunner.exe1.config" -Raw }
Mock Get-Content -ParameterFilter { $Path -eq "C:\Program Files\Microsoft\Exchange Server\V15\Bin\EdgeTransport.exe.config" } -MockWith { Get-Content "$Script:MockDataCollectionRoot\Exchange\EdgeTransport.exe1.config" -Raw }
@@ -308,7 +308,7 @@ Describe "Testing Health Checker by Mock Data Imports" {
-MockWith { return Import-Clixml "$Script:MockDataCollectionRoot\Hardware\Physical_Win32_PhysicalMemory.xml" }
Mock Get-WmiObjectHandler -ParameterFilter { $Class -eq "Win32_Processor" } `
-MockWith { return Import-Clixml "$Script:MockDataCollectionRoot\Hardware\Physical_Win32_Processor1.xml" }
- Mock Get-ExSetupDetails { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup1.xml" }
+ Mock Get-ExSetupFileVersionInfo { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup1.xml" }
Mock Invoke-ScriptBlockHandler -ParameterFilter { $ScriptBlockDescription -eq "Getting applicationHost.config" } -MockWith { return Get-Content "$Script:MockDataCollectionRoot\Exchange\IIS\applicationHost2.config" -Raw }
SetDefaultRunOfHealthChecker "Debug_Scenario3_Physical_Results.xml"
diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1
index 4f6592f638..aeb7b399ab 100644
--- a/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1
+++ b/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1
@@ -24,7 +24,7 @@ Describe "Testing Health Checker by Mock Data Imports" {
Mock Get-ExchangeServer { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetExchangeServer.xml" }
Mock Get-ExchangeCertificate { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetExchangeCertificate.xml" }
Mock Get-AuthConfig { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetAuthConfig.xml" }
- Mock Get-ExSetupDetails { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup.xml" }
+ Mock Get-ExSetupFileVersionInfo { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup.xml" }
Mock Get-MailboxServer { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetMailboxServer.xml" }
Mock Get-OwaVirtualDirectory { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetOwaVirtualDirectory.xml" }
Mock Get-WebServicesVirtualDirectory { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetWebServicesVirtualDirectory.xml" }
@@ -60,7 +60,7 @@ Describe "Testing Health Checker by Mock Data Imports" {
Assert-MockCalled Get-WmiObjectHandler -Exactly 6
Assert-MockCalled Invoke-ScriptBlockHandler -Exactly 5
- Assert-MockCalled Get-RemoteRegistryValue -Exactly 23
+ Assert-MockCalled Get-RemoteRegistryValue -Exactly 24
Assert-MockCalled Get-NETFrameworkVersion -Exactly 1
Assert-MockCalled Get-DotNetDllFileVersions -Exactly 1
Assert-MockCalled Get-NicPnpCapabilitiesSetting -Exactly 1
@@ -79,7 +79,7 @@ Describe "Testing Health Checker by Mock Data Imports" {
Assert-MockCalled Get-ExchangeServer -Exactly 1
Assert-MockCalled Get-ExchangeCertificate -Exactly 1
Assert-MockCalled Get-AuthConfig -Exactly 1
- Assert-MockCalled Get-ExSetupDetails -Exactly 1
+ Assert-MockCalled Get-ExSetupFileVersionInfo -Exactly 1
Assert-MockCalled Get-MailboxServer -Exactly 1
Assert-MockCalled Get-OwaVirtualDirectory -Exactly 1
Assert-MockCalled Get-WebServicesVirtualDirectory -Exactly 1
diff --git a/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1 b/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1
index 54176f01cd..e1e76593dc 100644
--- a/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1
+++ b/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1
@@ -121,6 +121,7 @@ Mock Get-RemoteRegistryValue {
"DaylightStart" { return @(0, 0, 3, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0) }
"DisableBaseTypeCheckForDeserialization" { return $null }
"DisablePreservation" { return 0 }
+ "DatabasePath" { return "$Script:MockDataCollectionRoot\Exchange" }
default { throw "Failed to find GetValue: $GetValue" }
}
}
@@ -220,7 +221,7 @@ Mock Get-ExchangeADSplitPermissionsEnabled {
return $false
}
-Mock Get-ExSetupDetails {
+Mock Get-ExSetupFileVersionInfo {
return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\ExSetup.xml"
}
diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1
new file mode 100644
index 0000000000..f57e98403e
--- /dev/null
+++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1
@@ -0,0 +1,244 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+function Invoke-TextExtractionOverride {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [string[]]$ComputerName,
+
+ [Parameter(Mandatory = $false)]
+ [string[]]$ConfigureOverride,
+
+ [string]$Action,
+
+ [switch]$Rollback
+ )
+ begin {
+ $remoteScriptBlockExecute = {
+ param($ArgumentList)
+ . $PSScriptRoot\..\..\..\..\Shared\Get-RemoteRegistryValue.ps1
+ . $PSScriptRoot\..\..\Shared\Invoke-StartStopService.ps1
+ . $PSScriptRoot\..\..\Shared\Invoke-XmlConfigurationRemoteAction.ps1
+
+ $VerbosePreference = $Using:VerbosePreference # To be able to write back to the host screen if -Verbose is used.
+ $successfulExecution = $false
+ $errorContext = New-Object System.Collections.Generic.List[object]
+ $activityBase = "[$env:COMPUTERNAME]"
+ $writeProgressParams = @{
+ Activity = "$activityBase Getting FIP FS Database Path"
+ Id = [Math]::Abs(($env:COMPUTERNAME).GetHashCode())
+ }
+
+ try {
+
+ if ($null -eq $ArgumentList -or
+ (($null -eq $ArgumentList.Rollback -or $false -eq $ArgumentList.Rollback) -and
+ ($null -eq $ArgumentList.ConfigureOverride -or $null -eq $ArgumentList.Action))) {
+ throw "Invalid ArgumentList provided to remote execution."
+ }
+
+ # We need to hard code this, which isn't ideal. But this is the best option that we have at the moment.
+ $defaultTypeLocations = @{
+ "XlsbOfficePackage" = "Excel"
+ "XlsmOfficePackage" = "Excel"
+ "XlsxOfficePackage" = "Excel"
+ "ExcelStorage" = "Excel"
+ "DocmOfficePackage" = "PreferIFilters"
+ "DocxOfficePackage" = "PreferIFilters"
+ "PptmOfficePackage" = "PreferIFilters"
+ "PptxOfficePackage" = "PreferIFilters"
+ "WordStorage" = "PreferIFilters"
+ "PowerPointStorage" = "PreferIFilters"
+ "VisioStorage" = "PreferIFilters"
+ "Rtf" = "PreferIFilters"
+ "Xml" = "PreferIFilters"
+ "OdfTextDocument" = "PreferIFilters"
+ "OdfSpreadsheet" = "PreferIFilters"
+ "OdfPresentation" = "PreferIFilters"
+ "OneNote" = "PreferIFilters"
+ "Pdf" = "PreferOutsideIn"
+ "Html" = "PreferOutsideIn"
+ "AutoCad" = "OutsideInOnly"
+ "Jpeg" = "OutsideInOnly"
+ "Tiff" = "OutsideInOnly"
+ }
+
+ $baseXPathFilter = "//*[local-name()='Configuration']/*[local-name()='System']/*[local-name()='TextExtractionSettings']"
+ $outsideInOnlyModuleXPathFilter = $baseXPathFilter +
+ "/*[local-name()='ModuleLists']/*[local-name()='ModuleList'][@TypeList='OutsideInOnly']/*[local-name()='Module'][contains(., 'OutsideInModule.dll')]"
+ $typeListBaseXPathFilter = $baseXPathFilter + "/*[local-name()='TypeLists']/*[local-name()='TypeList'][@Name='{0}']"
+ $getTypeBaseTypeListXPathFilter = $baseXPathFilter +
+ "/*[local-name()='TypeLists']/*[local-name()='TypeList']/*[local-name()='Type'][starts-with(@Name, '{0}')]"
+ Write-Progress @writeProgressParams
+
+ $fipFsDatabaseParams = @{
+ MachineName = $env:COMPUTERNAME
+ SubKey = "SOFTWARE\Microsoft\ExchangeServer\v15\FIP-FS"
+ GetValue = "DatabasePath"
+ }
+ $fipFsDatabasePath = Get-RemoteRegistryValue @fipFsDatabaseParams
+
+ if (([string]::IsNullOrEmpty($fipFsDatabasePath))) {
+ throw "Unable to find FIP FS Database Path"
+ }
+
+ $path = (Join-Path $fipFsDatabasePath "Configuration.xml")
+ Write-Verbose "Using the database path of '$path' to adjust"
+
+ $xmlConfigurationRemoteAction = [PSCustomObject]@{
+ FilePath = $path
+ BackupFileName = "TextExtractionOverride"
+ Actions = (New-Object System.Collections.Generic.List[object])
+ }
+
+ $writeProgressParams.Activity = $activityBase + " Stopping MSExchangeTransport and FMS Services"
+ Write-Progress @writeProgressParams
+ # Always Stop the services first
+ # TODO: Determine if we need to stop the services or need to restart
+ $serviceResult = Invoke-StartStopService -ServiceName "MSExchangeTransport", "FMS" -Action "Stop"
+
+ if ($true -eq $serviceResult) {
+
+ if (-not $ArgumentList.Rollback) {
+ # If we got a true result, we stopped the service
+ # Now create the actions list
+ foreach ($configureActionOverride in $ArgumentList.ConfigureOverride) {
+ if ($configureActionOverride -eq "OutsideInModule") {
+ # If configureActionOverride is OutsideInModule then we are setting that path only.
+ $actionOperation = [PSCustomObject]@{
+ SelectNodesFilter = $outsideInOnlyModuleXPathFilter
+ OperationType = [string]::Empty
+ Operation = [PSCustomObject]@{
+ AttributeName = "#text"
+ Value = "|NO"
+ ReplaceValue = [string]::Empty
+ }
+ }
+
+ if ($ArgumentList.Action -eq "Allow") {
+ $actionOperation.OperationType = "AppendAttribute"
+ $xmlConfigurationRemoteAction.Actions.Add($actionOperation)
+ } elseif ($ArgumentList.Action -eq "Block") {
+ $actionOperation.OperationType = "ReplaceAttributeValue"
+ $xmlConfigurationRemoteAction.Actions.Add($actionOperation)
+ }
+ } else {
+ # Now everything else is attempting to do the following on the Type:
+ # Either set or remove the |NO flag
+ # Move the Type to the TypeList OutsideInOnly as that is the only location where the |NO flag is honored
+ $baseFilter = $getTypeBaseTypeListXPathFilter -f $configureActionOverride
+
+ if ($ArgumentList.Action -eq "Allow") {
+
+ $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{
+ SelectNodesFilter = $baseFilter
+ OperationType = "MoveNode"
+ Operation = [PSCustomObject]@{
+ MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f "OutsideInOnly")
+ ParentNodeAttributeNameFilterAdd = "Name"
+ }
+ }))
+
+ $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{
+ SelectNodesFilter = $baseFilter
+ OperationType = "AppendAttribute"
+ Operation = [PSCustomObject]@{
+ AttributeName = "Name"
+ Value = "|NO"
+ }
+ }))
+ } elseif ($ArgumentList.Action -eq "Block") {
+ $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{
+ SelectNodesFilter = $baseFilter
+ OperationType = "ReplaceAttributeValue"
+ Operation = [PSCustomObject]@{
+ AttributeName = "Name"
+ Value = "|NO"
+ ReplaceValue = [string]::Empty
+ }
+ }))
+
+ $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{
+ SelectNodesFilter = $baseFilter
+ OperationType = "MoveNode"
+ Operation = [PSCustomObject]@{
+ MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f $defaultTypeLocations[$configureActionOverride])
+ ParentNodeAttributeNameFilterAdd = "Name"
+ }
+ }))
+ }
+ }
+ }
+ } else {
+ $xmlConfigurationRemoteAction | Add-Member -MemberType NoteProperty -Name "Restore" -Value ([PSCustomObject]@{
+ FileName = $xmlConfigurationRemoteAction.BackupFileName
+ })
+ }
+
+ # Now that we have the list of actions, we need to execute the results then determine if we were successful or not.
+ $writeProgressParams.Activity = $activityBase + " updating the Xml Configuration file"
+ Write-Progress @writeProgressParams
+ $results = Invoke-XmlConfigurationRemoteAction -InputObject $xmlConfigurationRemoteAction
+ Write-Host ""
+
+ if ($results.SuccessfulExecution) {
+ Write-Host "[$env:COMPUTERNAME] Successfully completed the configuration for FIP FS Text Extraction Override"
+ $successfulExecution = $true
+ } else {
+ Write-Warning "$env:COMPUTERNAME Failed to execution configuration action for FIP FS Text Extraction Override"
+ }
+
+ Write-Host ""
+ }
+
+ # Attempt to start the service again, even if we failed to stop. One could have worked.
+ $writeProgressParams.Activity = $activityBase + " Starting MSExchangeTransport and FMS Services"
+ Write-Progress @writeProgressParams
+ $serviceResult = Invoke-StartStopService -ServiceName "MSExchangeTransport", "FMS" -Action "Start"
+ } catch {
+ Write-Verbose "Caught an exception while trying to execute actions for Text EXtraction Override. Inner Exception: $_"
+ $errorContext.Add($_)
+ } finally {
+ Write-Progress @writeProgressParams -Completed
+ [PSCustomObject]@{
+ ServerName = $env:COMPUTERNAME
+ SuccessfulExecution = $successfulExecution
+ ErrorContext = $errorContext
+ ServicesStarted = $true -eq $serviceResult
+ }
+ }
+ }
+ }
+ process {
+ $results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $remoteScriptBlockExecute -ArgumentList ([PSCustomObject]@{
+ ConfigureOverride = $ConfigureOverride
+ Action = $Action
+ Rollback = $Rollback
+ })
+
+ $successServers = @($results | Where-Object { $_.SuccessfulExecution -eq $true -and $_.ErrorContext.Count -eq 0 })
+ $failedServers = @($results | Where-Object { $_.SuccessfulExecution -eq $false -or $_.ErrorContext.Count -ne 0 })
+ $failedServiceStart = @($results | Where-Object { $_.ServicesStarted -eq $false })
+
+ Write-Host ""
+ Write-Host ""
+
+ if ($null -ne $failedServers -and
+ $failedServers.Count -gt 0) {
+ Write-Warning "Failed to complete Text Extraction Override on the following servers: $([string]::Join(", ", $failedServers.ServerName))"
+ }
+
+ if ($null -ne $failedServiceStart -and
+ $failedServiceStart.Count -gt 0) {
+ Write-Warning "Failed to start the MSExchangeTransport and/or FMS services on the following servers: $([string]::Join(", ", $failedServiceStart.ServerName))"
+ }
+
+ if ($null -ne $successServers -and
+ $successServers.Count -gt 0) {
+ Write-Host "Successfully completed Text Extraction Override on the following servers: $([string]::Join(", ", $successServers.ServerName))" -ForegroundColor "Green"
+ }
+
+ Write-Host ""
+ }
+}
diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1
new file mode 100644
index 0000000000..d50592bf96
--- /dev/null
+++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1
@@ -0,0 +1,186 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+.SYNOPSIS
+ This script can be used revert the 'secure by default' change which was introduced as part of the Exchange Server March 2024 security update.
+ More information can be found in https://support.microsoft.com/help/5036795
+.DESCRIPTION
+ The script can be used to add overrides to the FIP-FS configuration.xml file.
+ This can be done to reactivate the use of the OutsideInModule for file types, which are no longer processed by the help of this module.
+ An override can also be done to the version of the OutsideInModule.dll. After the March 2024 security update was installed,
+ Exchange Server uses OutsideInModule version 8.5.7 by default, which is the latest version that was available at the time the SU was published.
+ By the help of the script, usage of the previous version 8.5.3 can be enforced.
+.PARAMETER ExchangeServerNames
+ Use this parameter to specify the Exchange Server on which the change to the configuration should be done.
+.PARAMETER SkipExchangeServerNames
+ Use this parameter to specify the Exchange Server, which should be excluded from the configuration action.
+.PARAMETER ConfigureOverride
+ Use this parameter to specify the file types for which the override should be added or from which the override should be removed.
+ You can also use this parameter to configure the override of the OutsideInModule version.
+ The values are case sensitive. Values that can be used with this parameter are:
+ OutsideInModule, XlsbOfficePackage, XlsmOfficePackage, XlsxOfficePackage, ExcelStorage , DocmOfficePackage,
+ DocxOfficePackage, PptmOfficePackage, PptxOfficePackage, WordStorage, PowerPointStorage, VisioStorage, Rtf,
+ Xml, OdfTextDocument, OdfSpreadsheet, OdfPresentation, OneNote, Pdf, Html, AutoCad, Jpeg, Tiff
+.PARAMETER Action
+ Use this parameter to specify the action that should be performed. The override flag will be added if the Allow value was used.
+ Values that can be passed are: Allow, Block
+ The default value is: Block
+.PARAMETER Rollback
+ Use this parameter to restore the configuration.xml based on the backup that was automatically created during a previous run of the script.
+ The restore operation will fail if no backup file can be found.
+.PARAMETER ScriptUpdateOnly
+ This optional parameter allows you to only update the script without performing any other actions.
+.PARAMETER SkipVersionCheck
+ This optional parameter allows you to skip the automatic version check and script update.
+.EXAMPLE
+ PS C:\> .\ConfigureFipFsTextExtractionOverrides.ps1 -ConfigureOverride OutsideInModule -Action Allow
+ It will add the override flag to the OutsideInModule.dll which is defined in the 'OutsideInOnly' module list. The action will be performed on
+ the machine where the script was executed.
+.EXAMPLE
+ PS C:\> .\ConfigureFipFsTextExtractionOverrides.ps1 -ConfigureOverride OutsideInModule -Action Block
+ It will remove the override flag from the OutsideInModule.dll which is defined in the 'OutsideInOnly' module list. The action will be performed on
+ the machine where the script was executed.
+.EXAMPLE
+ PS C:\> .\ConfigureFipFsTextExtractionOverrides.ps1 -ConfigureOverride AutoCad -Action Allow
+ It will add the override flag to the 'AutoCad' file type. The action will be performed on the machine where the script was executed.
+.EXAMPLE
+ PS C:\> Get-ExchangeServer | .\ConfigureFipFsTextExtractionOverrides.ps1 -ConfigureOverride AutoCad -Action Allow
+ It will add the override flag to the 'AutoCad' file type. The action will be performed on all Exchange servers.
+.EXAMPLE
+ PS C:\> Get-ExchangeServer | .\ConfigureFipFsTextExtractionOverrides.ps1 -Rollback -SkipExchangeServerNames "ExchSrv02"
+ It will restore the configuration.xml from the backup file that was created during a previous run of the script.
+ The action will be performed on all Exchange servers except ExchSrv02.
+#>
+
+[CmdletBinding(DefaultParameterSetName = "ConfigureOverride", SupportsShouldProcess = $true, ConfirmImpact = 'High')]
+param(
+ [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName = "ConfigureOverride")]
+ [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName = "Rollback")]
+ [string[]]$ExchangeServerNames,
+
+ [Parameter(Mandatory = $false, ParameterSetName = "ConfigureOverride")]
+ [Parameter(Mandatory = $false, ParameterSetName = "Rollback")]
+ [string[]]$SkipExchangeServerNames,
+
+ [Parameter(Mandatory = $true, ParameterSetName = "ConfigureOverride")]
+ [ValidateSet("OutsideInModule", "XlsbOfficePackage", "XlsmOfficePackage", "XlsxOfficePackage", "ExcelStorage" , "DocmOfficePackage",
+ "DocxOfficePackage", "PptmOfficePackage", "PptxOfficePackage", "WordStorage", "PowerPointStorage", "VisioStorage", "Rtf",
+ "Xml", "OdfTextDocument", "OdfSpreadsheet", "OdfPresentation", "OneNote", "Pdf", "Html", "AutoCad", "Jpeg", "Tiff", IgnoreCase = $false)]
+ [string[]]$ConfigureOverride,
+
+ [Parameter(Mandatory = $false, ParameterSetName = "ConfigureOverride")]
+ [ValidateSet("Allow", "Block")]
+ [string]$Action = "Block",
+
+ [Parameter(Mandatory = $true, ParameterSetName = "Rollback")]
+ [switch]$Rollback,
+
+ [Parameter(Mandatory = $false, ParameterSetName = "ScriptUpdateOnly")]
+ [switch]$ScriptUpdateOnly,
+
+ [switch]$SkipVersionCheck
+)
+
+begin {
+ $versionsUrl = "https://aka.ms/ConfigureFipFsTextExtractionOverrides-VersionsURL"
+
+ . $PSScriptRoot\ConfigurationAction\Invoke-TextExtractionOverride.ps1
+ . $PSScriptRoot\..\Shared\Get-ProcessedServerList.ps1
+ . $PSScriptRoot\..\..\..\Shared\Confirm-ExchangeManagementShell.ps1
+ . $PSScriptRoot\..\..\..\Shared\GenericScriptStartLogging.ps1
+ . $PSScriptRoot\..\..\..\Shared\ScriptUpdateFunctions\GenericScriptUpdate.ps1
+
+ $includeExchangeServerNames = New-Object System.Collections.Generic.List[string]
+} process {
+ foreach ($server in $ExchangeServerNames) {
+ $includeExchangeServerNames.Add($server)
+ }
+} end {
+ try {
+ Write-Verbose "Url to check for new versions of the script is: $versionsUrl"
+
+ if (-not (Confirm-ExchangeManagementShell)) {
+ Write-Error "This script must be run from Exchange Management Shell."
+ exit
+ }
+
+ if ($ConfigureOverride.Count -gt 1 -and $ConfigureOverride -contains "OutsideInModule") {
+ Write-Error "OutsideInModule ConfigureOverride can only be processed by itself."
+ exit
+ }
+
+ if ($includeExchangeServerNames.Count -eq 0 -and
+ ($null -eq $SkipExchangeServerNames -or $SkipExchangeServerNames.Count -eq 0)) {
+ Write-Host "Only going to attempt to run against the local server '$($env:COMPUTERNAME)' since no servers were provided."
+ $includeExchangeServerNames.Add($env:COMPUTERNAME)
+ }
+
+ $exchangeServicesWording = "Each Exchange server's MSExchangeTransport and FMS service will be restarted to backup and apply the configuration change."
+ $vulnerabilityMoreInformationWording = "More information about the security vulnerability can be found here: https://portal.msrc.microsoft.com/security-guidance/advisory/ADV24199947."
+
+ if ($ConfigureOverride -eq "OutsideInModule" -and
+ $Action -eq "Allow") {
+ $params = @{
+ Message = "Display warning about OutsideInModule override operation"
+ Target = "This operation enables an outdate version of the OutsideInModule which is known to be vulnerable." +
+ "`r`n$exchangeServicesWording" +
+ "`r`n$vulnerabilityMoreInformationWording" +
+ "`r`nDo you want to proceed?"
+ Operation = "Enabling usage of an outdated OutsideInModule version"
+ }
+ } elseif ($ConfigureOverride.Count -ge 1 -and
+ $Action -eq "Allow") {
+ $params = @{
+ Message = "Display warning about file type override operation"
+ Target = "This operation enables OutsideInModule usage for the following file types:" +
+ "`r`n$([string]::Join(", ", $ConfigureOverride))" +
+ "`r`n$exchangeServicesWording" +
+ "`r`n$vulnerabilityMoreInformationWording" +
+ "`r`nDo you want to proceed?"
+ Operation = "Configure file types that should be processed by the OutsideInModule"
+ }
+ } else {
+ $params = @{
+ Message = "Display warning about service restart operation"
+ Target = "$exchangeServicesWording" +
+ "`r`nDo you want to proceed?"
+ Operation = "Performing OutsideInModule configuration action"
+ }
+ }
+
+ Show-Disclaimer @params
+
+ $processParams = @{
+ ExchangeServerNames = $includeExchangeServerNames
+ SkipExchangeServerNames = $SkipExchangeServerNames
+ CheckOnline = $true
+ DisableGetExchangeServerFullList = $includeExchangeServerNames.Count -gt 0 # if we pass a list, we shouldn't need to get all the servers in the org.
+ MinimumSU = "Mar24SU"
+ }
+
+ $processedExchangeServers = Get-ProcessedServerList @processParams
+
+ $params = @{
+ ComputerName = $processedExchangeServers.ValidExchangeServerFqdn
+ ConfigureOverride = $ConfigureOverride
+ Action = $Action
+ Rollback = $Rollback
+ }
+
+ if ($Rollback -and $processedExchangeServers.OutdatedBuildExchangeServerFqdn.Count -gt 0) {
+ Write-Host "Adding the Server(s) back into the list to process because we are attempting to rollback: $([string]::Join(", ", $processedExchangeServers.OutdatedBuildExchangeServerFqdn))"
+ $params.ComputerName = $processedExchangeServers.OnlineExchangeServerFqdn
+ }
+
+ if ($params.ComputerName.Count -ge 1) {
+ Write-Host "Running the configuration change against the following server(s): $([string]::Join(", ", $params.ComputerName))"
+ Invoke-TextExtractionOverride @params
+ } else {
+ Write-Host "None of the server(s) passed to the script do support OutsideInModule overrides"
+ }
+ } finally {
+ Write-Host ""
+ Write-Host "Do you have feedback regarding the script? Please let us know: ExToolsFeedback@microsoft.com."
+ }
+}
diff --git a/Security/src/Shared/Get-ProcessedServerList.ps1 b/Security/src/Shared/Get-ProcessedServerList.ps1
new file mode 100644
index 0000000000..d41f261cd9
--- /dev/null
+++ b/Security/src/Shared/Get-ProcessedServerList.ps1
@@ -0,0 +1,143 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+. $PSScriptRoot\..\..\..\Shared\CompareExchangeBuildLevel.ps1
+. $PSScriptRoot\..\..\..\Shared\Get-ExchangeBuildVersionInformation.ps1
+. $PSScriptRoot\..\..\..\Shared\Get-ExSetupFileVersionInfo.ps1
+. $PSScriptRoot\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1
+
+function Get-ProcessedServerList {
+ [CmdletBinding()]
+ param(
+ [string[]]$ExchangeServerNames,
+
+ [string[]]$SkipExchangeServerNames,
+
+ [bool]$CheckOnline,
+
+ [bool]$DisableGetExchangeServerFullList,
+
+ [string]$MinimumSU
+ )
+ begin {
+ Write-Verbose "Calling: $($MyInvocation.MyCommand)"
+ # The complete list of all the Exchange Servers that we ran Get-ExchangeServer against.
+ $getExchangeServer = New-Object System.Collections.Generic.List[object]
+ # The list of possible validExchangeServers prior to completing the list.
+ $possibleValidExchangeServer = New-Object System.Collections.Generic.List[object]
+ # The Get-ExchangeServer object for all the servers that are either in ExchangeServerNames or not in SkipExchangeServerNames and are within the correct SU build.
+ $validExchangeServer = New-Object System.Collections.Generic.List[object]
+ # The FQDN of the servers in the validExchangeServer list
+ $validExchangeServerFqdn = New-Object System.Collections.Generic.List[string]
+ # Servers that are online within the validExchangeServer list.
+ $onlineExchangeServer = New-Object System.Collections.Generic.List[object]
+ # The FQDN of the servers that are in the onlineExchangeServer list
+ $onlineExchangeServerFqdn = New-Object System.Collections.Generic.List[string]
+ # The list of servers that are outside min required SU
+ $outdatedBuildExchangeServerFqdn = New-Object System.Collections.Generic.List[string]
+ }
+ process {
+ if ($DisableGetExchangeServerFullList) {
+ # If we don't want to get all the Exchange Servers, then we need to make sure the list of Servers are Exchange Server
+ if ($null -eq $ExchangeServerNames -or
+ $ExchangeServerNames.Count -eq 0) {
+ throw "Must provide servers to process when DisableGetExchangeServerFullList is set."
+ }
+
+ Write-Verbose "Getting the result of the Exchange Servers individually"
+ foreach ($server in $ExchangeServerNames) {
+ try {
+ $result = Get-ExchangeServer $server -ErrorAction Stop
+ $getExchangeServer.Add($result)
+ } catch {
+ Write-Verbose "Failed to run Get-ExchangeServer for server '$server'. Inner Exception $_"
+ throw
+ }
+ }
+ } else {
+ Write-Verbose "Getting all the Exchange Servers in the organization"
+ $result = @(Get-ExchangeServer)
+ $getExchangeServer.AddRange($result)
+ }
+
+ if ($null -ne $ExchangeServerNames -and $ExchangeServerNames.Count -gt 0) {
+ $getExchangeServer |
+ Where-Object { ($_.Name -in $ExchangeServerNames) -or ($_.FQDN -in $ExchangeServerNames) } |
+ ForEach-Object {
+ if ($null -ne $SkipExchangeServerNames -and $SkipExchangeServerNames.Count -gt 0) {
+ if (($_.Name -notin $SkipExchangeServerNames) -and ($_.FQDN -notin $SkipExchangeServerNames)) {
+ Write-Verbose "Adding Server $($_.Name) to the valid server list"
+ $possibleValidExchangeServer.Add($_)
+ }
+ } else {
+ Write-Verbose "Adding Server $($_.Name) to the valid server list"
+ $possibleValidExchangeServer.Add($_)
+ }
+ }
+ } else {
+ if ($null -ne $SkipExchangeServerNames -and $SkipExchangeServerNames.Count -gt 0) {
+ $getExchangeServer |
+ Where-Object { ($_.Name -notin $SkipExchangeServerNames) -and ($_.FQDN -notin $SkipExchangeServerNames) } |
+ ForEach-Object {
+ Write-Verbose "Adding Server $($_.Name) to the valid server list"
+ $possibleValidExchangeServer.Add($_)
+ }
+ } else {
+ Write-Verbose "Adding Server $($_.Name) to the valid server list"
+ $possibleValidExchangeServer.AddRange($getExchangeServer)
+ }
+ }
+
+ if ($CheckOnline -or (-not ([string]::IsNullOrEmpty($MinimumSU)))) {
+ Write-Verbose "Will check to see if the servers are online"
+ foreach ($server in $possibleValidExchangeServer) {
+ $exSetupDetails = Get-ExSetupFileVersionInfo -Server $server.FQDN
+
+ if ($null -ne $exSetupDetails -and
+ (-not ([string]::IsNullOrEmpty($exSetupDetails)))) {
+ # Got some results back, they are online.
+ $onlineExchangeServer.Add($server)
+ $onlineExchangeServerFqdn.Add($Server.FQDN)
+
+ if (-not ([string]::IsNullOrEmpty($MinimumSU))) {
+ $params = @{
+ CurrentExchangeBuild = (Get-ExchangeBuildVersionInformation -FileVersion $exSetupDetails.FileVersion)
+ SU = $MinimumSU
+ }
+ if ((Test-ExchangeBuildGreaterOrEqualThanSecurityPatch @params)) {
+ $validExchangeServer.Add($server)
+ } else {
+ Write-Verbose "Server $($server.Name) build is older than our expected min SU build. Build Number: $($exSetupDetails.FileVersion)"
+ $outdatedBuildExchangeServerFqdn.Add($server.FQDN)
+ }
+ } else {
+ $validExchangeServer.Add($server)
+ }
+ } else {
+ Write-Verbose "Server $($server.Name) not online"
+ }
+ }
+ } else {
+ $validExchangeServer.AddRange($possibleValidExchangeServer)
+ }
+
+ $validExchangeServer | ForEach-Object { $validExchangeServerFqdn.Add($_.FQDN) }
+
+ # If we have servers in the outdatedBuildExchangeServerFqdn list, the default response should be to display that we are removing them from the list.
+ if ($outdatedBuildExchangeServerFqdn.Count -gt 0) {
+ Write-Host ""
+ Write-Host "Excluded the following server(s) because the build is older than what is required to make a change: $([string]::Join(", ", $outdatedBuildExchangeServerFqdn))"
+ Write-Host ""
+ }
+ }
+ end {
+ return [PSCustomObject]@{
+ ValidExchangeServer = $validExchangeServer
+ ValidExchangeServerFqdn = $validExchangeServerFqdn
+ GetExchangeServer = $getExchangeServer
+ OnlineExchangeServer = $onlineExchangeServer
+ OnlineExchangeServerFqdn = $onlineExchangeServerFqdn
+ OutdatedBuildExchangeServerFqdn = $outdatedBuildExchangeServerFqdn
+ }
+ }
+}
diff --git a/Security/src/Shared/Invoke-StartStopService.ps1 b/Security/src/Shared/Invoke-StartStopService.ps1
new file mode 100644
index 0000000000..385cc6d9f3
--- /dev/null
+++ b/Security/src/Shared/Invoke-StartStopService.ps1
@@ -0,0 +1,42 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+.DESCRIPTION
+This is used to start or stop services locally on the server. It will return a value of $true if we do not hit an exception.
+If an exception does occur, a throw will occur so the caller needs to handle this.
+#>
+function Invoke-StartStopService {
+ [CmdletBinding()]
+ [OutputType([bool])]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string[]]$ServiceName,
+
+ [Parameter(Mandatory = $true)]
+ [ValidateSet("Start", "Stop")]
+ [string]$Action
+ )
+ process {
+ Write-Verbose "Calling: $($MyInvocation.MyCommand)"
+ try {
+ if ($Action -eq "Stop") {
+ Write-Verbose "Stopping Services: $([string]::Join(", ", $ServiceName))"
+ foreach ($name in $ServiceName) {
+ Stop-Service -Name $name -Force -ErrorAction Stop
+ }
+ } else {
+ Write-Verbose "Starting Services: $([string]::Join(", ", $ServiceName))"
+ foreach ($name in $ServiceName) {
+ Start-Service -Name $name -ErrorAction Stop
+ }
+ }
+ } catch {
+ Write-Verbose "Unable able to perform $Action action on the server. Inner Exception $_"
+ # caller should be handling the exceptions that are occurring. So we should throw if we run into an issue.
+ throw
+ }
+
+ return $true
+ }
+}
diff --git a/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1 b/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1
new file mode 100644
index 0000000000..bf10ed817a
--- /dev/null
+++ b/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1
@@ -0,0 +1,558 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+TODO List
+- Verify the PassWhatIf is needed.
+- Provide detail description of process and docs
+- Add in Write-Process logic
+- Try to use json file instead of xml for restore process
+- Determine log logic
+
+#>
+
+<#
+.DESCRIPTION
+ Execute the configuration actions on the remote server. This is the script block to be sent to the server.
+
+ InputObject
+ [string]FilePath
+ [object[]]Actions
+ [string]SelectNodesFilter
+ [string]OperationType AcceptedValues: RemoveNode, SetAttribute, AppendAttribute, MoveNode, ReplaceAttributeValue
+ [object]Operation
+ Type = SetAttribute
+ [string]AttributeName
+ [string]Value
+ Type = AppendAttribute
+ [string]AttributeName
+ [string]Value
+ Type = ReplaceAttributeValue
+ [string]AttributeName
+ [string]Value
+ [string]ReplaceValue
+ Type = MoveNode
+ [string]MoveToSelectNodesFilter
+
+ # This is only required if the SelectNodesFilter doesn't contain a narrow filtered request where only 1 node is returned.
+ [string]ParentNodeAttributeNameFilterAdd
+ [string]BackupFileName
+ [object]Restore
+ [string]FileName
+ [bool]PassedWhatIf ?? need this?
+#>
+function Invoke-XmlConfigurationRemoteAction {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [object]$InputObject
+ )
+ begin {
+
+ function TestLastChildNodeRestoreAction {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [string]$LastChildNode,
+
+ [Parameter(Mandatory)]
+ [string]$AttributeName,
+
+ [Parameter(Mandatory)]
+ [string]$CurrentValue,
+
+ [Parameter(Mandatory)]
+ [string]$NewValue,
+
+ [Parameter(Mandatory)]
+ [ref]$RestoreAction
+ )
+
+ # If the Current SelectNodesFilter that we are using to track down the Node contains a filter for an exact match for the attribute that we are manipulating
+ # We need to properly process the change for the restore process to work.
+ $splitResults = $LastChildNode.Split("[").Split("]")
+
+ if ($splitResults -contains "@$AttributeName='$CurrentValue'" -or
+ $splitResults -contains "@$AttributeName=`"$CurrentValue`"") {
+ if ($LastChildNode.IndexOf($CurrentValue) -ne $LastChildNode.LastIndexOf($CurrentValue)) {
+ throw "Last child node contains multiple entries for the current value. Unable to determine new filter to use on restore."
+ }
+
+ $updatedReplaceChildNode = $LastChildNode.Replace($CurrentValue, $NewValue)
+ $RestoreAction.Value.SelectNodesFilter = $RestoreAction.Value.SelectNodesFilter.Replace($LastChildNode, $updatedReplaceChildNode)
+ Write-Verbose "Updated SelectNodesFilter to: $($RestoreAction.Value.SelectNodesFilter)"
+ }
+ }
+
+ Write-Verbose "Calling: $($MyInvocation.MyCommand)"
+ $isRestoreOption = $null -ne $InputObject.Restore
+ $errorContext = New-Object System.Collections.Generic.List[object]
+ $restoreActions = New-Object System.Collections.Generic.List[object]
+ $originalContent = New-Object System.Collections.Generic.List[object]
+ $allActionsPerformed = $true
+ $gatheredAllRestoreActions = $true
+ $saveRawContent = $false
+ $restoreActionsSaved = $isRestoreOption -eq $true
+ $alreadySaveRestoreActions = $null
+ $backupRestoreFilePath = [string]::Empty
+ $validationFailed = $false
+ $rootSavePath = [System.IO.Path]::GetDirectoryName($InputObject.FilePath)
+ $restoreFileName = "XmlConfigurationRestoreCmdlets-{0}.xml"
+
+ if ($isRestoreOption) {
+ $fileName = $restoreFileName -f $InputObject.Restore.FileName
+ $backupRestoreFilePath = [System.IO.Path]::Combine($rootSavePath, $fileName)
+ } elseif (-not ([string]::IsNullOrEmpty($InputObject.BackupFileName))) {
+ $fileName = $restoreFileName -f $InputObject.BackupFileName
+ $backupRestoreFilePath = [System.IO.Path]::Combine($rootSavePath, $fileName)
+ }
+ }
+ process {
+ <#
+ Restore Xml Structure
+ [object[]]OriginalContent
+ [int]Id
+ [object]Content
+ [object[]]Actions
+ [string]RestoreType AcceptedValues: AppendChild, SetAttribute, MoveNode
+ [string]SelectNodesFilter This should always be the location where we want to handle actions in the main configuration file.
+ [object]Operation
+ Type = AppendChild
+ [int]ContentId
+ [string]OriginalSelectNodesFilter
+ Type = SetAttribute
+ [string]AttributeName
+ [string]RestoreValue
+ Type = MoveNode
+ [string]MoveToSelectNodesFilter
+ #>
+ try {
+ Write-Verbose "-------------------------------------------------"
+ Write-Verbose "Starting Xml Configuration$(if($isRestoreOption){ " Restore" }) Action: $([DateTime]::Now)"
+ Write-Verbose "-------------------------------------------------"
+
+ # Verify all the actions to make sure they are valid.
+ foreach ($action in $InputObject.Actions) {
+ try {
+ if ([string]::IsNullOrEmpty($action.SelectNodesFilter)) {
+ throw "Failed to provide action SelectNodesFilter value."
+ }
+
+ if ($null -eq $action.OperationType -or
+ ($action.OperationType -ne "RemoveNode" -and
+ $action.OperationType -ne "SetAttribute" -and
+ $action.OperationType -ne "AppendAttribute" -and
+ $action.OperationType -ne "ReplaceAttributeValue" -and
+ $action.OperationType -ne "MoveNode")) {
+ throw "Failed to provide valid action OperationType."
+ }
+
+ if (($null -eq $action.Operation -and $action.OperationType -ne "RemoveNode") -or
+ (($action.OperationType -eq "SetAttribute" -or
+ $action.OperationType -eq "AppendAttribute") -and
+ ([string]::IsNullOrEmpty($action.Operation.AttributeName) -or
+ [string]::IsNullOrEmpty($action.Operation.Value))) -or
+ ($action.OperationType -eq "ReplaceAttributeValue" -and
+ ([string]::IsNullOrEmpty($action.Operation.AttributeName) -or
+ [string]::IsNullOrEmpty($action.Operation.Value) -or
+ $null -eq $action.Operation.ReplaceValue)) -or
+ ($action.OperationType -eq "MoveNode" -and
+ ([string]::IsNullOrEmpty($action.Operation.MoveToSelectNodesFilter)))) {
+ throw "Failed to provide correct Operation values for OperationType '$($action.OperationType)'"
+ }
+ } catch {
+ Write-Verbose "Failed to provide valid Actions object structure. Inner Exception: $_"
+ $errorContext.Add($_)
+ $validationFailed = $true
+ }
+ }
+
+ if (-not (Test-Path $InputObject.FilePath)) {
+ $validationFailed = $true
+ Write-Verbose "Incorrect FilePath provided. '$($InputObject.FilePath)'"
+ $errorContext.Add((New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Configuration File Path Not Found: '$($InputObject.FilePath)'"))
+ }
+
+ if (-not $isRestoreOption -and [string]::IsNullOrEmpty($InputObject.BackupFileName)) {
+ $validationFailed = $true
+ Write-Verbose "BackupFileName was not set."
+ $errorContext.Add((New-Object -TypeName System.Exception -ArgumentList "BackupFileName not set on input object."))
+ }
+
+ if ($validationFailed) { return }
+
+ try {
+ $contentRaw = Get-Content $InputObject.FilePath -ErrorAction Stop -Raw
+ [xml]$contentXml = $contentRaw
+ } catch {
+ Write-Verbose "Failed to load the configuration file. Inner Exception: $_"
+ $errorContext.Add($_)
+ return
+ }
+
+ # attempt to load the current backup file if it exists
+ if (-not ([string]::IsNullOrEmpty($backupRestoreFilePath))) {
+ if ((Test-Path $backupRestoreFilePath)) {
+ Write-Verbose "Backup/Restore file already exists. Attempting to load it."
+
+ try {
+ $alreadySaveRestoreActions = Import-Clixml $backupRestoreFilePath -ErrorAction Stop
+
+ # Should really look to see if there are multiple matches already.
+ Write-Verbose "Adding pre-saved restore actions to memory"
+ foreach ($value in $alreadySaveRestoreActions.Actions) {
+ Write-Verbose "RestoreType: $($value.RestoreType) SelectNodesFilter: $($value.SelectNodesFilter)"
+ $restoreActions.Add($value)
+ }
+
+ Write-Verbose "Loading previous configuration files"
+ foreach ($content in $alreadySaveRestoreActions.OriginalContent) {
+ Write-Verbose "Loading Content ID $($content.Id)"
+ $originalContent.Add($content)
+ }
+ } catch {
+ Write-Verbose "Failed to load the current backup file '$backupRestoreFilePath'."
+ $errorContext.Add($_)
+ throw "Failed to load the current backup file. Inner Exception: $_"
+ }
+ } else {
+ Write-Verbose "No Backup/Restore file exists at: '$($backupRestoreFilePath)'"
+
+ if ($isRestoreOption) {
+ Write-Error "Unable to restore due to no restore file. '$backupRestoreFilePath'"
+ # Must throw to break out and prevent from moving forward
+ throw "No restore file exists: '$($backupRestoreFilePath)'"
+ }
+ }
+ }
+
+ if ($isRestoreOption) {
+ # Don't need to worry about if the restore file wasn't there. This was already handled
+ Write-Verbose "Starting Restore Process"
+ $restoreActions.Reverse() # Reverse the order to make sure that the restore process should always work, unless manually modifying the files to where we can't find the nodes.
+ foreach ($action in $restoreActions) {
+ try {
+ Write-Verbose "Trying to find nodes based off filter: '$($action.SelectNodesFilter)'"
+ $selectNodes = $contentXml.SelectNodes($action.SelectNodesFilter)
+ Write-Verbose "Found $($selectNodes.Count) node(s)"
+
+ if ($selectNodes.Count -eq 0) {
+ Write-Verbose "No nodes were found with the current filter. Unable to perform restore action. Filter: $($action.SelectNodesFilter)"
+ # TODO: Determine how to handle
+ continue
+ }
+
+ if ($selectNodes.Count -gt 1) {
+ throw "Multiple nodes found in restore process for filter '$($action.SelectNodesFilter)'. Unable to continue."
+ } else {
+ $selectNode = $selectNodes[0] # This is required to be able to do AppendChild
+ }
+
+ if ($action.RestoreType -eq "AppendChild") {
+ # This restore type we have to find the node to restore from the saved configuration content.
+ Write-Verbose "Attempting to find the original content by Id $($action.Operation.ContentId)"
+ $content = ($originalContent | Where-Object { $_.Id -eq $action.Operation.ContentId }).Content
+
+ if ($null -eq $content) {
+ throw "No Restore Content Found for the AppendChild to restore."
+ }
+
+ # Now we need to find the node again.
+ $restoreSelectNodes = ([xml]$content).SelectNodes($action.Operation.OriginalSelectNodesFilter)
+
+ if ($null -eq $restoreSelectNodes) {
+ throw "Failed to find the OriginalSelectNodesFilter: '$($action.Operation.OriginalSelectNodesFilter)'"
+ }
+
+ # Possible multiple nodes? Need to look into this
+ foreach ($node in $restoreSelectNodes) {
+ $importNode = $contentXml.ImportNode($node, $true)
+ [void]$selectNode.AppendChild($importNode)
+ }
+ } elseif ($action.RestoreType -eq "SetAttribute") {
+ if ($null -eq $selectNode.($action.Operation.AttributeName)) {
+ throw "Attribute '$($action.Operation.AttributeName)' currently doesn't exist on node."
+ }
+
+ Write-Verbose "Setting attribute '$($action.Operation.AttributeName)' with value of '$($action.Operation.RestoreValue)'"
+ $selectNode.($action.Operation.AttributeName) = $action.Operation.RestoreValue
+ } elseif ($action.RestoreType -eq "MoveNode") {
+ $moveToNodeLocation = $contentXml.SelectNodes($action.Operation.MoveToSelectNodesFilter)
+
+ if ($moveToNodeLocation.Count -eq 0) {
+ throw "Failed to find node selection to move to"
+ }
+
+ if ($moveToNodeLocation.Count -gt 1) {
+ throw "Found multiple node locations to move to. This is unsupported."
+ }
+
+ [void]$selectNode.ParentNode.RemoveChild($selectNode)
+ [void]$moveToNodeLocation.AppendChild($selectNode)
+ }
+ } catch {
+ $allActionsPerformed = $false
+ Write-Verbose "Failed to restore a setting. Inner Exception: $_"
+ $errorContext.Add($_)
+ }
+ }
+
+ if ($errorContext.Count -gt 0) {
+ Write-Warning "Errors occurred preventing the restore from completing."
+ return
+ }
+
+ try {
+ # Now try to save out the file
+ $contentXml.Save($InputObject.FilePath)
+ } catch {
+ $allActionsPerformed = $false
+ Write-Verbose "Failed to save configuration file. Inner exception: $_"
+ $errorContext.Add($_)
+ return
+ }
+
+ try {
+ Remove-Item $backupRestoreFilePath -Force -ErrorAction Stop
+ Write-Verbose "Successfully removed the restore file."
+ } catch {
+ $allActionsPerformed = $false
+ Write-Verbose "Failed to remove the restore file. Inner Exception: $_"
+ $errorContext.Add($_)
+ return
+ }
+
+ return
+ }
+
+ # for each action provided, do the action.
+ foreach ($action in $InputObject.Actions) {
+ Write-Verbose "Trying to find SelectNodes based off filter: '$($action.SelectNodesFilter)'"
+ $selectNodes = $contentXml.SelectNodes($action.SelectNodesFilter)
+
+ if ($selectNodes.Count -eq 0) {
+ # This shouldn't be treated as an error.
+ Write-Verbose "No nodes were found with the current filter. This could be the action was already taken or doesn't exist."
+ continue
+ }
+
+ <#
+ It is ideal to always narrow down your filter so only 1 item is returned.
+ This is going to be a requirement if the Action is to set anything other than RemoveNode.
+ However, if the calculated parent node select filter would return multiple nodes, then we will also throw an issue.
+ This is to prevent any issues with the restore process and making sure that only the correct setting gets added back to the correct location.
+ #>
+ Write-Verbose "Found $($selectNodes.Count) Node(s)"
+
+ if ($selectNodes.Count -gt 1 -and
+ $action.OperationType -ne "RemoveNode") {
+ throw "Multiple Nodes found with filter '$($action.SelectNodesFilter)'. This breaks the restore logic and are unable to continue."
+ }
+
+ foreach ($node in $selectNodes) {
+
+ try {
+
+ $currentRestoreAction = [PSCustomObject]@{
+ RestoreType = "NotSet"
+ SelectNodesFilter = [string]::Empty
+ Operation = $null
+ }
+
+ if ($action.OperationType -eq "RemoveNode") {
+
+ $lastIndexOf = $action.SelectNodesFilter.LastIndexOf("/")
+
+ if ($lastIndexOf -eq -1) {
+ throw "Failed to provide a filter that would have a parent node to restore to."
+ }
+
+ $parentSelectNodesFilter = $action.SelectNodesFilter.Substring(0, $lastIndexOf)
+ $testSelectNodesResults = $contentXml.SelectNodes($parentSelectNodesFilter)
+
+ if ($testSelectNodesResults.Count -eq 0) {
+ throw "No parent nodes where found. This shouldn't occur. Unable to continue."
+ }
+
+ if ($testSelectNodesResults.Count -gt 1) {
+ throw "Multiple nodes returned for parent node which will result in restore process failure. Unable to continue."
+ }
+
+ Write-Verbose "Parent Select Nodes Filter Passed: $($parentSelectNodesFilter)"
+ $currentRestoreAction.RestoreType = "AppendChild"
+ $currentRestoreAction.SelectNodesFilter = $parentSelectNodesFilter
+ $currentRestoreAction.Operation = [PSCustomObject]@{
+ ContentId = $originalContent.Count
+ OriginalSelectNodesFilter = $action.SelectNodesFilter
+ }
+ # Need to handle What if scenario here
+ [void]$node.ParentNode.RemoveChild($node)
+ Write-Verbose "Successfully removed node."
+ } elseif ($action.OperationType -eq "MoveNode") {
+ $lastChildNode = $action.SelectNodesFilter.Substring($action.SelectNodesFilter.LastIndexOf("/"))
+ $moveToNodeLocation = $contentXml.SelectNodes($action.Operation.MoveToSelectNodesFilter)
+
+ if ($moveToNodeLocation.Count -eq 0) {
+ throw "Failed to find node selection to move to"
+ }
+
+ if ($moveToNodeLocation.Count -gt 1) {
+ throw "Found multiple node locations to move to. This is unsupported."
+ }
+
+ if ([string]::IsNullOrEmpty($action.Operation.ParentNodeAttributeNameFilterAdd)) {
+ $moveToSelectNodesFilter = $action.SelectNodesFilter.Replace($lastChildNode, "")
+ } else {
+ $moveToSelectNodesFilter = $action.SelectNodesFilter.Replace($lastChildNode, "") +
+ "[@$($action.Operation.ParentNodeAttributeNameFilterAdd)='$($node.ParentNode.($action.Operation.ParentNodeAttributeNameFilterAdd))']"
+ }
+
+ $currentRestoreAction.RestoreType = "MoveNode"
+ $currentRestoreAction.SelectNodesFilter = $action.Operation.MoveToSelectNodesFilter + $lastChildNode
+ $currentRestoreAction.Operation = [PSCustomObject]@{
+ MoveToSelectNodesFilter = $moveToSelectNodesFilter
+ }
+
+ # Now verify that we can move it back for the restore process.
+ Write-Verbose "Verifying possible restore process with filter: $($currentRestoreAction.Operation.MoveToSelectNodesFilter)"
+ $verifyMoveBack = $contentXml.SelectNodes($currentRestoreAction.Operation.MoveToSelectNodesFilter)
+
+ if ($verifyMoveBack.Count -ne 1) {
+ throw "Found multiple node locations for the move back. Since we are unable to restore, preventing move from occurring."
+ }
+
+ [void]$node.ParentNode.RemoveChild($node)
+ [void]$moveToNodeLocation.AppendChild($node)
+ } elseif ($action.OperationType -eq "SetAttribute" -or
+ $action.OperationType -eq "AppendAttribute" -or
+ $action.OperationType -eq "ReplaceAttributeValue") {
+
+ if ($null -eq $node.($action.Operation.AttributeName)) {
+ throw "Attribute '$($action.Operation.AttributeName)' doesn't exist on this node"
+ }
+
+ $currentRestoreAction.RestoreType = "SetAttribute"
+ $currentRestoreAction.SelectNodesFilter = $action.SelectNodesFilter
+ $currentRestoreAction.Operation = [PSCustomObject]@{
+ AttributeName = $action.Operation.AttributeName
+ RestoreValue = $node.($action.Operation.AttributeName)
+ }
+ Write-Verbose "Stored the current value of the attribute. '$($currentRestoreAction.Operation.RestoreValue)'"
+
+ if ($action.OperationType -eq "AppendAttribute") {
+ $currentValue = $node.($action.Operation.AttributeName)
+ # If currentValue already has what we are trying to append with, don't do anything.
+ if ($currentValue.EndsWith($action.Operation.Value)) {
+ Write-Verbose "Already have the appended value, skipping over action"
+ continue
+ }
+ $newAppendValue = $node.($action.Operation.AttributeName) + $action.Operation.Value
+ $params = @{
+ LastChildNode = $action.SelectNodesFilter.Substring($action.SelectNodesFilter.LastIndexOf("/"))
+ AttributeName = $action.Operation.AttributeName
+ CurrentValue = $currentValue
+ NewValue = $newAppendValue
+ RestoreAction = [ref]$currentRestoreAction
+ }
+ TestLastChildNodeRestoreAction @params
+ $node.($action.Operation.AttributeName) = $newAppendValue
+ } elseif ($action.OperationType -eq "ReplaceAttributeValue") {
+ # With this operation, we need to treat this similar as AppendAttribute value with handling the restore process
+ $currentValue = $node.($action.Operation.AttributeName)
+ $newReplaceValue = $currentValue.Replace($action.Operation.Value, $action.Operation.ReplaceValue)
+
+ $params = @{
+ LastChildNode = $action.SelectNodesFilter.Substring($action.SelectNodesFilter.LastIndexOf("/"))
+ AttributeName = $action.Operation.AttributeName
+ CurrentValue = $currentValue
+ NewValue = $newReplaceValue
+ RestoreAction = [ref]$currentRestoreAction
+ }
+ TestLastChildNodeRestoreAction @params
+ $node.($action.Operation.AttributeName) = $newReplaceValue
+ } else {
+ # Need to handle what if scenario here
+ $node.($action.Operation.AttributeName) = $action.Operation.Value
+ Write-Verbose "Successfully reset the value to '$($action.Operation.Value)'"
+ }
+ }
+
+ # Add Current Restore to list if needed.
+ if ($null -ne $alreadySaveRestoreActions) {
+ $matchFound = $null -ne ($restoreActions |
+ Where-Object { $_.RestoreType -eq $currentRestoreAction.RestoreType -and
+ $_.SelectNodesFilter -eq $currentRestoreAction.SelectNodesFilter })
+ }
+
+ if ($null -eq $alreadySaveRestoreActions -or $matchFound -eq $false) {
+ Write-Verbose "Adding new restore action"
+ $restoreActions.Add($currentRestoreAction)
+
+ # Since we are adding a new restore action, we need to check to see if the action requires you to save the original content
+ if ($currentRestoreAction.RestoreType -eq "AppendChild") {
+ $saveRawContent = $true
+ }
+ } else {
+ Write-Verbose "Found match, don't overwrite setting. Not adding to restore action"
+ }
+ } catch {
+ Write-Verbose "Ran into an exception while executing the actions. Inner Exception: $_"
+ $errorContext.Add($_)
+ # Determine if we want to break out of here.
+ }
+ }
+ }
+
+ # If there has been an error, we don't want to continue.
+ if ($errorContext.Count -gt 0) { return }
+
+ try {
+ if ($saveRawContent) {
+ $originalContent.Add(([PSCustomObject]@{
+ Id = $originalContent.Count
+ Content = $contentRaw
+ }))
+ }
+ $restoreActionResults = [PSCustomObject]@{
+ OriginalContent = $originalContent
+ Actions = $restoreActions
+ }
+ # Maybe we don't want to save if nothing new was added.
+ # Save out the restore action prior to saving the configuration file.
+ $restoreActionResults | Export-Clixml -Path $backupRestoreFilePath -Encoding utf8 -Force -ErrorAction Stop
+ Write-Verbose "Successfully saved out the restore actions to path: $backupRestoreFilePath"
+ $restoreActionsSaved = $true
+ } catch {
+ Write-Verbose "Unable to export Restore Actions. Inner Exception: $_"
+ $errorContext.Add($_)
+ return
+ }
+
+ try {
+ $contentXml.Save($InputObject.FilePath)
+ Write-Verbose "Successfully saved out the configuration file to path: $($InputObject.FilePath)"
+ } catch {
+ Write-Verbose "Failed to save the updated configuration file. Inner Exception: $_"
+ $errorContext.Add($_)
+ }
+ } catch {
+ Write-Verbose "Failed to compete Xml Configuration Execution. Inner Exception: $_"
+ $errorContext.Add($_)
+ return
+ }
+ }
+ end {
+ Write-Verbose "Ending Xml Configuration$(if($isRestoreOption) { " Restore"}) Action: $([DateTime]::Now)"
+ Write-Verbose "-------------------------------------------------"
+
+ return [PSCustomObject]@{
+ ComputerName = $env:COMPUTERNAME
+ AllActionsPerformed = $allActionsPerformed
+ GatheredAllRestoreActions = $gatheredAllRestoreActions
+ RestoreActions = $restoreActions
+ RestoreActionsSaved = $restoreActionsSaved
+ SuccessfulExecution = $allActionsPerformed -and $gatheredAllRestoreActions -and $restoreActionsSaved -and $errorContext.Count -eq 0
+ ErrorContext = $errorContext
+ }
+ }
+}
diff --git a/Diagnostics/HealthChecker/Helpers/CompareExchangeBuildLevel.ps1 b/Shared/CompareExchangeBuildLevel.ps1
similarity index 98%
rename from Diagnostics/HealthChecker/Helpers/CompareExchangeBuildLevel.ps1
rename to Shared/CompareExchangeBuildLevel.ps1
index 9fa467159f..9062b3f577 100644
--- a/Diagnostics/HealthChecker/Helpers/CompareExchangeBuildLevel.ps1
+++ b/Shared/CompareExchangeBuildLevel.ps1
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
-. $PSScriptRoot\..\..\..\Shared\Get-ExchangeBuildVersionInformation.ps1
+. $PSScriptRoot\Get-ExchangeBuildVersionInformation.ps1
function Test-ExchangeBuildGreaterOrEqualThanBuild {
[CmdletBinding()]
[OutputType([bool])]
diff --git a/Shared/GenericScriptStartLogging.ps1 b/Shared/GenericScriptStartLogging.ps1
new file mode 100644
index 0000000000..3ee99b3871
--- /dev/null
+++ b/Shared/GenericScriptStartLogging.ps1
@@ -0,0 +1,48 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+ This file is designed to inline code that we use to start the scripts and handle the logging.
+#>
+
+. $PSScriptRoot\OutputOverrides\Write-Host.ps1
+. $PSScriptRoot\OutputOverrides\Write-Progress.ps1
+. $PSScriptRoot\OutputOverrides\Write-Verbose.ps1
+. $PSScriptRoot\OutputOverrides\Write-Warning.ps1
+. $PSScriptRoot\Confirm-Administrator.ps1
+. $PSScriptRoot\LoggerFunctions.ps1
+. $PSScriptRoot\Show-Disclaimer.ps1
+
+function Write-DebugLog ($Message) {
+ $Script:DebugLogger = $Script:DebugLogger | Write-LoggerInstance $Message
+}
+
+function Write-HostLogAndDebugLog ($Message) {
+ $Script:Logger = $Script:Logger | Write-LoggerInstance $Message
+ Write-DebugLog $Message
+}
+
+$Script:DebugLogger = Get-NewLoggerInstance -LogName "$($script:MyInvocation.MyCommand.Name)-Debug"
+
+SetWriteVerboseAction ${Function:Write-DebugLog}
+SetWriteProgressAction ${Function:Write-DebugLog}
+SetWriteWarningAction ${Function:Write-DebugLog}
+
+# Dual Logging is for when you have a secondary file designed for debug logic and one that is simplified for everything that was displayed to the screen.
+Write-Verbose "Dual Logging $(if(-not ($Script:DualLoggingEnabled)){ "NOT "})Enabled."
+if ($Script:DualLoggingEnabled) {
+ $params = @{
+ LogName = ([System.IO.Path]::GetFileNameWithoutExtension($Script:DebugLogger.FullPath).Replace("-Debug", ""))
+ AppendDateTime = $false
+ AppendDateTimeToFileName = $false
+ }
+ $Script:Logger = Get-NewLoggerInstance @params
+ SetWriteHostAction ${Function:Write-HostLogAndDebugLog}
+} else {
+ SetWriteHostAction ${Write-DebugLog}
+}
+
+if (-not(Confirm-Administrator)) {
+ Write-Host "The script needs to be executed in elevated mode. Start the PowerShell as an administrator." -ForegroundColor Yellow
+ exit
+}
diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1 b/Shared/Get-ExSetupFileVersionInfo.ps1
similarity index 85%
rename from Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1
rename to Shared/Get-ExSetupFileVersionInfo.ps1
index ea82172f3f..d2ee7d0230 100644
--- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1
+++ b/Shared/Get-ExSetupFileVersionInfo.ps1
@@ -1,11 +1,14 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
-. $PSScriptRoot\..\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1
-function Get-ExSetupDetails {
+. $PSScriptRoot\Invoke-ScriptBlockHandler.ps1
+function Get-ExSetupFileVersionInfo {
param(
[Parameter(Mandatory = $true)]
- [string]$Server
+ [string]$Server,
+
+ [Parameter(Mandatory = $false)]
+ [ScriptBlock]$CatchActionFunction
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
@@ -27,7 +30,7 @@ function Get-ExSetupDetails {
}
}
- $exSetupDetails = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock ${Function:Get-ExSetupDetailsScriptBlock} -ScriptBlockDescription "Getting ExSetup remotely" -CatchActionFunction ${Function:Invoke-CatchActions}
+ $exSetupDetails = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock ${Function:Get-ExSetupDetailsScriptBlock} -ScriptBlockDescription "Getting ExSetup remotely" -CatchActionFunction $CatchActionFunction
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $exSetupDetails
}
diff --git a/Shared/Get-ExchangeBuildVersionInformation.ps1 b/Shared/Get-ExchangeBuildVersionInformation.ps1
index 9f14b8d71e..4a9fafb8da 100644
--- a/Shared/Get-ExchangeBuildVersionInformation.ps1
+++ b/Shared/Get-ExchangeBuildVersionInformation.ps1
@@ -126,22 +126,21 @@ function Get-ExchangeBuildVersionInformation {
$cuLevel = "CU14"
$cuReleaseDate = "02/13/2024"
$supportedBuildNumber = $true
- $latestSUBuild = $true
}
+ (GetBuildVersion $ex19 "CU14" -SU "Mar24SU") { $latestSUBuild = $true }
{ $_ -lt (GetBuildVersion $ex19 "CU14") } {
$cuLevel = "CU13"
$cuReleaseDate = "05/03/2023"
$supportedBuildNumber = $true
$orgValue = 16761
}
- (GetBuildVersion $ex19 "CU13" -SU "Nov23SU") { $latestSUBuild = $true }
+ (GetBuildVersion $ex19 "CU13" -SU "Mar24SU") { $latestSUBuild = $true }
{ $_ -lt (GetBuildVersion $ex19 "CU13") } {
$cuLevel = "CU12"
$cuReleaseDate = "04/20/2022"
$supportedBuildNumber = $false
$orgValue = 16760
}
- (GetBuildVersion $ex19 "CU12" -SU "Nov23SU") { $latestSUBuild = $true }
{ $_ -lt (GetBuildVersion $ex19 "CU12") } {
$cuLevel = "CU11"
$cuReleaseDate = "09/28/2021"
@@ -227,7 +226,7 @@ function Get-ExchangeBuildVersionInformation {
$cuReleaseDate = "04/20/2022"
$supportedBuildNumber = $true
}
- (GetBuildVersion $ex16 "CU23" -SU "Nov23SU") { $latestSUBuild = $true }
+ (GetBuildVersion $ex16 "CU23" -SU "Mar24SU") { $latestSUBuild = $true }
{ $_ -lt (GetBuildVersion $ex16 "CU23") } {
$cuLevel = "CU22"
$cuReleaseDate = "09/28/2021"
@@ -711,6 +710,7 @@ function GetExchangeBuildDictionary {
"Aug23SUv2" = "15.1.2507.32"
"Oct23SU" = "15.1.2507.34"
"Nov23SU" = "15.1.2507.35"
+ "Mar24SU" = "15.1.2507.37"
})
}
"Exchange2019" = @{
@@ -808,8 +808,11 @@ function GetExchangeBuildDictionary {
"Aug23SUv2" = "15.2.1258.25"
"Oct23SU" = "15.2.1258.27"
"Nov23SU" = "15.2.1258.28"
+ "Mar24SU" = "15.2.1258.32"
+ })
+ "CU14" = (NewCUAndSUObject "15.2.1544.4" @{
+ "Mar24SU" = "15.2.1544.9"
})
- "CU14" = (NewCUAndSUObject "15.2.1544.4")
}
}
}
diff --git a/Shared/ScriptUpdateFunctions/GenericScriptUpdate.ps1 b/Shared/ScriptUpdateFunctions/GenericScriptUpdate.ps1
new file mode 100644
index 0000000000..03571f8b37
--- /dev/null
+++ b/Shared/ScriptUpdateFunctions/GenericScriptUpdate.ps1
@@ -0,0 +1,44 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+ This set of code is designed to handle updating your script as this code is basically the same everywhere, making this a common file to avoid duplication.
+ Just need to dot load the file to your script and have the correct parameters, then this code does the work for you.
+ These are the parameters that you should have within your script.
+ This needs to be done within the main part of the script, not inside a function to work correctly.
+
+ [Parameter(Mandatory = $false, ParameterSetName = "ScriptUpdateOnly")]
+ [switch]$ScriptUpdateOnly,
+
+ [switch]$SkipVersionCheck
+#>
+
+. $PSScriptRoot\Test-ScriptVersion.ps1
+
+$BuildVersion = ""
+Write-Host ("$($script:MyInvocation.MyCommand.Name) script version $($BuildVersion)") -ForegroundColor Green
+
+$scriptVersionParams = @{
+ AutoUpdate = $true
+ Confirm = $false
+}
+
+# This needs to be set prior to injecting this file to other scripts.
+if (-not ([string]::IsNullOrEmpty($versionsUrl))) {
+ $scriptVersionParams.Add("VersionsUrl", $versionsUrl)
+}
+
+if ($ScriptUpdateOnly) {
+ switch (Test-ScriptVersion @scriptVersionParams) {
+ ($true) { Write-Host ("Script was successfully updated") -ForegroundColor Green }
+ ($false) { Write-Host ("No update of the script performed") -ForegroundColor Yellow }
+ default { Write-Host ("Unable to perform ScriptUpdateOnly operation") -ForegroundColor Red }
+ }
+ exit
+}
+
+if ((-not($SkipVersionCheck)) -and
+ (Test-ScriptVersion @scriptVersionParams)) {
+ Write-Host ("Script was updated. Please re-run the command") -ForegroundColor Yellow
+ exit
+}
diff --git a/docs/Emerging-Issues.md b/docs/Emerging-Issues.md
index faabb78775..583615c04c 100644
--- a/docs/Emerging-Issues.md
+++ b/docs/Emerging-Issues.md
@@ -7,24 +7,26 @@ hide:
This page lists emerging issues for Exchange On-Premises deployments, possible root cause and solution/workaround to fix the issues. The page will be consistently updated with new issues found and reflect current status of the issues mentioned.
-|**Updated on** | **Update causing the issue**| **Issue**| **Workaround/Solution**
+|**Updated on**|**Update causing the issue**|**Issue**|**Workaround/Solution**|
|-|-|-|-|
-2/20/2024 | [CU 14 for Exchange 2019](https://support.microsoft.com/KB/5035606) | Environments that are using SSL offloading configuration may face issues with Outlook connectivity issues after upgrading to Exchange 2019 CU14. | As announced in [August 2023](https://techcommunity.microsoft.com/t5/exchange-team-blog/coming-soon-enabling-extended-protection-on-exchange-server-by/ba-p/3911849) , by default, starting with CU14, Setup enables the Windows Extended Protection (EP) feature on the Exchange server being installed. Extended Protection isn't supported in environments that use SSL Offloading. SSL termination during SSL Offloading causes Extended Protection to fail. To enable Extended Protection in your Exchange environment, you must not be using SSL offloading with your Load Balancers. Please check [this link](https://learn.microsoft.com/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-extended-protection?view=exchserver-2019#scenarios-that-could-affect-client-connectivity-when-extended-protection-was-enabled) for more details
-2/19/2024 | [CU 14 for Exchange 2019](https://support.microsoft.com/topic/5036404) | Exchange 2019 CU14 RecoverServer fails while creating "New-PushNotificationsVirtualDirectory" with following error:
Exception setting "ExtendedProtectionTokenChecking": "Cannot convert null to type "Microsoft.Exchange.Data.Directory.SystemConfiguration.ExtendedProtectionTokenCheckingMode" due to enumeration values that are not valid.
| Please follow the steps from [this KB](https://support.microsoft.com/topic/5036404) to resolve the issue
-11/23/2023 | [November 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2023-exchange-server-security-updates/ba-p/3980209) for Exchange 2016, Exchange 2019 | Some customers may find queue viewer crashing with error
"Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints" | The error can occur if the Exchange server auth certificate has expired. Solution is to renew the [Exchange server auth certificate manually](https://learn.microsoft.com/exchange/troubleshoot/administration/cannot-access-owa-or-ecp-if-oauth-expired) or by using [this script](https://microsoft.github.io/CSS-Exchange/Admin/MonitorExchangeAuthCertificate/)
-10/12/2023|[All versions of August 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811) for Exchange 2016, Exchange 2019 | Users in account forest can't change expired password in OWA in multi-forest Exchange deployments after installing any version of [August 2023 Security Update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811)
**Note**
The account forest user will be able to change the password after they sign in to Outlook on the web if their password is not yet expired. The issue affects only account forest users who have passwords that are already expired. This change does not affect users in organizations that don't use multiple forests.|** Update on 10/12/2023 **
Follow steps on [this article](https://support.microsoft.com/topic/users-in-account-forest-can-t-change-expired-password-in-owa-in-multi-forest-exchange-deployments-after-installing-august-2023-su-b17c3579-0233-4d84-9245-755dd1092edb)
-8/15/2023|[Non-English August 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811) for Exchange 2016, Exchange 2019 | When you install the Microsoft Exchange Server 2019 or 2016 August 2023 Security Update (SU) on a Windows Server-based device that is running a non-English operating system (OS) version, Setup suddenly stops and rolls back the changes. However, the Exchange Server services remain in a disabled state. |The latest SUs have been released that do not require a workaround to install. If you used a workaround to install KB5029388, it is highly recommend to uninstall the KB5029388 to avoid issues down the line. For more information please check out [this KB](https://support.microsoft.com/topic/exchange-server-2019-and-2016-august-2023-security-update-installation-fails-on-non-english-operating-systems-ef38d805-f645-4511-8cc5-cf967e5d5c75).
-6/15/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | When you try to uninstall Microsoft Exchange Server 2019 or 2016 on servers, that had January 2023 Security Update for Exchange Server installed at any point, the Setup fails with following error message:
[ERROR] The operation couldn't be performed because object '' couldn't be found on ''. |Install Exchange Security Update June 2023 or higher to resolve the issue. Check [this KB](https://support.microsoft.com/help/5025312) for more details
-6/15/2023|Extended protection enabled on Exchange server | Changing the permissions for Public Folders by using an Outlook client will fail with the following error, if Extended Protection is enabled:
`The modified Permissions cannot be changed.`| Install Exchange Security Update June 2023 or higher Security Update and create the setting override mentioned in [this KB](https://support.microsoft.com/topic/extended-protection-doesn-t-support-public-folder-client-permissions-management-through-outlook-bd2037b5-40e0-413a-b368-746b3f5439ee)
-|3/16/2023| [Outlook client update for CVE-2023-23397 released](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23397)| These vulnerabilities affect Exchange Server. Exchange Online customers are already protected from the vulnerabilities addressed in these SUs and do not need to take any action **other than updating Exchange servers in their environment, and if applicable, installing the security update for Outlook on Windows described on the link on the right.**
More details about specific CVEs can be found in the [Security Update Guide](https://msrc.microsoft.com/update-guide/) (filter on Exchange Server under Product Family).
**Awareness: Outlook client update for CVE-2023-23397 released**
There is a critical security update for Microsoft Outlook for Windows that is required to address [CVE-2023-23397](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23397). To address this CVE, **you must install the Outlook security update, regardless of where your mail is hosted (e.g., Exchange Online, Exchange Server, some other platform).** | **Please check [this page](https://aka.ms/OLKCVEFAQ) for FAQs about the [Outlook CVE-2023-23397](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23397)**
-3/14/2023|[February 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-february-2023-exchange-server-security-updates/ba-p/3741058) for Exchange 2016, Exchange 2019, Exchange 2013 | After installing February 2023 security update, customers are seeing EWS application pool crash with Event ID 4999 with following error
E12IIS, c-RTL-AMD64, 15.01.2507.021, w3wp#MSExchangeServicesAppPool, M.Exchange.Diagnostics, M.E.D.ChainedSerializationBinder.EnforceBlockReason, M.E.Diagnostics.BlockedDeserializeTypeException, 437c-dumptidset, 15.01.2507.021.
The issue is causing connectivity issues to EWS based clients (Outlook for Mac) | **Update on 3/14/2023**
The issue is fixed in [March 2023 security update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2023-exchange-server-security-updates/ba-p/3764224)
Please follow the steps in [this KB](https://support.microsoft.com/help/5024257)
-3/14/2023|[February 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-february-2023-exchange-server-security-updates/ba-p/3741058) for Exchange 2016, Exchange 2019, Exchange 2013 | Some customers are reporting issues with Outlook/OWA add-ins, like add-in not listing in EAC or with the Get-App command. Additionally, they may notice EWS application pool crash with Event ID 4999 in the application log of the Exchange server. | **Update on 3/14/2023**
The issue is fixed in [March 2023 security update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2023-exchange-server-security-updates/ba-p/3764224)
-3/14/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 |The Exchange toolbox may start crashing on launch after [certificate Serialization for PowerShell](https://aka.ms/HC-SerializedDataSigning) is enabled. The error noticed is "Deserialization fails: System.Reflection.TargetInvocationException".
The issue happens only on Exchange 2016 and Exchange 2019| **Update on 3/14/2023**
The issue is fixed in [March 2023 security update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2023-exchange-server-security-updates/ba-p/3764224)
-|-|-|-|- |-|-|
-1/24/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | After installing January 2023 security update and enabling [certificate signing for serialization of PowerShell](https://aka.ms/HC-SerializedDataSigning), you may find various Exchange commands and scripts (example: RedistributeActiveDatabases.ps1) that use deserialization failing with the error similar to :
Error: "Cannot convert the value of type.....to type" | Use [this script](https://aka.ms/MonitorExchangeAuthCertificate) to update the auth certificate
-1/24/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | RecoverServer will fail at pre-requisites check with following error:
"Exchange Server version Version 15.1 (Build 2507.17) or later must be used to perform a recovery of this server." | **Update on 02/23/2023**
The issue has been fixed in [February 2023 Security Update for Exchange servers](https://support.microsoft.com/KB/5023038), however, the following workaround still needs to be used for servers that are on January 2023 Security Update
**Workaround**
Use the steps in [this](https://learn.microsoft.com/exchange/troubleshoot/setup/version-error-in-recover-server-mode-install) article
-1/24/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016 installed on Windows 2012 R2, other versions are not affected |The Exchange services in Automatic start-up mode will not start after reboot of the server. The services start successfully if started manually| **Update on 02/23/2023**
The issue has been fixed in [February 2023 Security Update for Exchange servers](https://support.microsoft.com/KB/5023038)
-1/24/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | Transport header shows the older version of server once January 2023 SU is installed (the build shown seems to be the build of the last CU) | The issue will be addressed in upcoming security update |
+| 3/14/2024 | [March 2024 Security Update for Exchange 2019,2016](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2024-exchange-server-security-updates/ba-p/4075348) | After installing the [March 2024 Security Update]((https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2024-exchange-server-security-updates/ba-p/4075348)), Search in Outlook (cached mode) may show "We're having trouble fetching results from the server...". The search works fine in OWA or Outlook online mode. | Use one of the following workarounds:
1) Use OWA or Outlook online mode for search
2) Disable server assisted search as described [here](https://support.microsoft.com/topic/5037277) |
+| 2/20/2024 | [CU 14 for Exchange 2019](https://support.microsoft.com/KB/5035606) | Environments that are using SSL offloading configuration may face issues with Outlook connectivity issues after upgrading to Exchange 2019 CU14. | As announced in [August 2023](https://techcommunity.microsoft.com/t5/exchange-team-blog/coming-soon-enabling-extended-protection-on-exchange-server-by/ba-p/3911849) , by default, starting with CU14, Setup enables the Windows Extended Protection (EP) feature on the Exchange server being installed. Extended Protection isn't supported in environments that use SSL Offloading. SSL termination during SSL Offloading causes Extended Protection to fail. To enable Extended Protection in your Exchange environment, you must not be using SSL offloading with your Load Balancers. Please check [this link](https://learn.microsoft.com/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-extended-protection?view=exchserver-2019#scenarios-that-could-affect-client-connectivity-when-extended-protection-was-enabled) for more details |
+| 2/20/2024 | [CU 14 for Exchange 2019](https://support.microsoft.com/KB/5035606)| Environments that are using SSL offloading configuration may face issues with Outlook connectivity issues after upgrading to Exchange 2019 CU14. | As announced in [August 2023](https://techcommunity.microsoft.com/t5/exchange-team-blog/coming-soon-enabling-extended-protection-on-exchange-server-by/ba-p/3911849) , by default, starting with CU14, Setup enables the Windows Extended Protection (EP) feature on the Exchange server being installed. Extended Protection isn't supported in environments that use SSL Offloading. SSL termination during SSL Offloading causes Extended Protection to fail. To enable Extended Protection in your Exchange environment, you must not be using SSL offloading with your Load Balancers. Please check [this link](https://learn.microsoft.com/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-extended-protection?view=exchserver-2019#scenarios-that-could-affect-client-connectivity-when-extended-protection-was-enabled) for more details |
+| 2/19/2024 | [CU 14 for Exchange 2019](https://support.microsoft.com/topic/5036404) | Exchange 2019 CU14 RecoverServer fails while creating "New-PushNotificationsVirtualDirectory" with following error:
Exception setting "ExtendedProtectionTokenChecking": "Cannot convert null to type "Microsoft.Exchange.Data.Directory.SystemConfiguration.ExtendedProtectionTokenCheckingMode" due to enumeration values that are not valid.
| Please follow the steps from [this KB](https://support.microsoft.com/topic/5036404) to resolve the issue
+| 11/23/2023 | [November 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2023-exchange-server-security-updates/ba-p/3980209) for Exchange 2016, Exchange 2019 | Some customers may find queue viewer crashing with error
"Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints" | The error can occur if the Exchange server auth certificate has expired. Solution is to renew the [Exchange server auth certificate manually](https://learn.microsoft.com/exchange/troubleshoot/administration/cannot-access-owa-or-ecp-if-oauth-expired) or by using [this script](https://microsoft.github.io/CSS-Exchange/Admin/MonitorExchangeAuthCertificate/) |
+| 10/12/2023 | [All versions of August 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811) for Exchange 2016, Exchange 2019 | Users in account forest can't change expired password in OWA in multi-forest Exchange deployments after installing any version of [August 2023 Security Update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811)
**Note**
The account forest user will be able to change the password after they sign in to Outlook on the web if their password is not yet expired. The issue affects only account forest users who have passwords that are already expired. This change does not affect users in organizations that don't use multiple forests.|** Update on 10/12/2023 **
Follow steps on [this article](https://support.microsoft.com/topic/users-in-account-forest-can-t-change-expired-password-in-owa-in-multi-forest-exchange-deployments-after-installing-august-2023-su-b17c3579-0233-4d84-9245-755dd1092edb) |
+| 8/15/2023 | [Non-English August 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811) for Exchange 2016, Exchange 2019 | When you install the Microsoft Exchange Server 2019 or 2016 August 2023 Security Update (SU) on a Windows Server-based device that is running a non-English operating system (OS) version, Setup suddenly stops and rolls back the changes. However, the Exchange Server services remain in a disabled state. |The latest SUs have been released that do not require a workaround to install. If you used a workaround to install KB5029388, it is highly recommend to uninstall the KB5029388 to avoid issues down the line. For more information please check out [this KB](https://support.microsoft.com/topic/exchange-server-2019-and-2016-august-2023-security-update-installation-fails-on-non-english-operating-systems-ef38d805-f645-4511-8cc5-cf967e5d5c75). |
+| 6/15/2023 | [January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | When you try to uninstall Microsoft Exchange Server 2019 or 2016 on servers, that had January 2023 Security Update for Exchange Server installed at any point, the Setup fails with following error message:
[ERROR] The operation couldn't be performed because object '' couldn't be found on ''. |Install Exchange Security Update June 2023 or higher to resolve the issue. Check [this KB](https://support.microsoft.com/help/5025312) for more details |
+| 6/15/2023 |Extended protection enabled on Exchange server | Changing the permissions for Public Folders by using an Outlook client will fail with the following error, if Extended Protection is enabled:
`The modified Permissions cannot be changed.`| Install Exchange Security Update June 2023 or higher Security Update and create the setting override mentioned in [this KB](https://support.microsoft.com/topic/extended-protection-doesn-t-support-public-folder-client-permissions-management-through-outlook-bd2037b5-40e0-413a-b368-746b3f5439ee) |
+| 3/16/2023 | [Outlook client update for CVE-2023-23397 released](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23397)| These vulnerabilities affect Exchange Server. Exchange Online customers are already protected from the vulnerabilities addressed in these SUs and do not need to take any action **other than updating Exchange servers in their environment, and if applicable, installing the security update for Outlook on Windows described on the link on the right.**
More details about specific CVEs can be found in the [Security Update Guide](https://msrc.microsoft.com/update-guide/) (filter on Exchange Server under Product Family).
**Awareness: Outlook client update for CVE-2023-23397 released**
There is a critical security update for Microsoft Outlook for Windows that is required to address [CVE-2023-23397](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23397). To address this CVE, **you must install the Outlook security update, regardless of where your mail is hosted (e.g., Exchange Online, Exchange Server, some other platform).** | **Please check [this page](https://aka.ms/OLKCVEFAQ) for FAQs about the [Outlook CVE-2023-23397](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23397)** |
+| 3/14/2023 | [February 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-february-2023-exchange-server-security-updates/ba-p/3741058) for Exchange 2016, Exchange 2019, Exchange 2013 | After installing February 2023 security update, customers are seeing EWS application pool crash with Event ID 4999 with following error
E12IIS, c-RTL-AMD64, 15.01.2507.021, w3wp#MSExchangeServicesAppPool, M.Exchange.Diagnostics, M.E.D.ChainedSerializationBinder.EnforceBlockReason, M.E.Diagnostics.BlockedDeserializeTypeException, 437c-dumptidset, 15.01.2507.021.
The issue is causing connectivity issues to EWS based clients (Outlook for Mac) | **Update on 3/14/2023**
The issue is fixed in [March 2023 security update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2023-exchange-server-security-updates/ba-p/3764224)
Please follow the steps in [this KB](https://support.microsoft.com/help/5024257) |
+| 3/14/2023 | [February 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-february-2023-exchange-server-security-updates/ba-p/3741058) for Exchange 2016, Exchange 2019, Exchange 2013 | Some customers are reporting issues with Outlook/OWA add-ins, like add-in not listing in EAC or with the Get-App command. Additionally, they may notice EWS application pool crash with Event ID 4999 in the application log of the Exchange server. | **Update on 3/14/2023**
The issue is fixed in [March 2023 security update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2023-exchange-server-security-updates/ba-p/3764224) |
+| 3/14/2023 | [January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 |The Exchange toolbox may start crashing on launch after [certificate Serialization for PowerShell](https://aka.ms/HC-SerializedDataSigning) is enabled. The error noticed is "Deserialization fails: System.Reflection.TargetInvocationException".
The issue happens only on Exchange 2016 and Exchange 2019| **Update on 3/14/2023**
The issue is fixed in [March 2023 security update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-march-2023-exchange-server-security-updates/ba-p/3764224) |
+|-|-|-|-|
+| 1/24/2023 | [January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | After installing January 2023 security update and enabling [certificate signing for serialization of PowerShell](https://aka.ms/HC-SerializedDataSigning), you may find various Exchange commands and scripts (example: RedistributeActiveDatabases.ps1) that use deserialization failing with the error similar to :
Error: "Cannot convert the value of type.....to type" | Use [this script](https://aka.ms/MonitorExchangeAuthCertificate) to update the auth certificate |
+| 1/24/2023 | [January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | RecoverServer will fail at pre-requisites check with following error:
"Exchange Server version Version 15.1 (Build 2507.17) or later must be used to perform a recovery of this server." | **Update on 02/23/2023**
The issue has been fixed in [February 2023 Security Update for Exchange servers](https://support.microsoft.com/KB/5023038), however, the following workaround still needs to be used for servers that are on January 2023 Security Update
**Workaround**
Use the steps in [this](https://learn.microsoft.com/exchange/troubleshoot/setup/version-error-in-recover-server-mode-install) article |
+| 1/24/2023 | [January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016 installed on Windows 2012 R2, other versions are not affected |The Exchange services in Automatic start-up mode will not start after reboot of the server. The services start successfully if started manually| **Update on 02/23/2023**
The issue has been fixed in [February 2023 Security Update for Exchange servers](https://support.microsoft.com/KB/5023038) |
+| 1/24/2023 | [January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | Transport header shows the older version of server once January 2023 SU is installed (the build shown seems to be the build of the last CU) | The issue will be addressed in upcoming security update |
diff --git a/docs/Security/ConfigureFipFsTextExtractionOverrides.md b/docs/Security/ConfigureFipFsTextExtractionOverrides.md
new file mode 100644
index 0000000000..f8fa71ed7c
--- /dev/null
+++ b/docs/Security/ConfigureFipFsTextExtractionOverrides.md
@@ -0,0 +1,72 @@
+# ConfigureFipFsTextExtractionOverrides
+
+Download the latest release: [ConfigureFipFsTextExtractionOverrides.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/ConfigureFipFsTextExtractionOverrides.ps1)
+
+!!! warning "Note"
+
+ Starting in the Exchange Server March 2024 security update we disable the use of the Oracle Outside In Technology (also known as OutsideInModule or OIT) in Microsoft Exchange Server due to multiple security vulnerabilities in the module. The OutsideInModule was used by the Microsoft Forefront Filtering Module to extract information from different file types, to perform content inspection as part of the Exchange Server Data Loss Prevention (DLP) or Exchange Transport Rules (ETR) features.
+
+The `ConfigureFipFsTextExtractionOverrides.ps1` script can be used to manipulate the usage of `OutsideInModule` that is disabled by default in the Exchange Server March 2024 security update.
+
+There are two scenarios in which the script could be used:
+
+- It can be used to explicitly enable file types that should be processed by the help of the `OutsideInModule`.
+- It can be used to override the version of the `OutsideInModule` that should be used for processing file types, which were explicitly enabled to be processed by the `OutsideInModule`. After installing the March 2024 security update, Exchange Server uses the latest version of the `OutsideInModule` version `8.5.7` by default. By activating this override, `OutsideInModule` version `8.5.3` will be used.
+
+Details about the change that was done as part of the March 2024 security update can be found in [KB5037191](https://support.microsoft.com/topic/5037191).
+
+Details about the security vulnerability can be found in the [MSRC security advisory](https://portal.msrc.microsoft.com/security-guidance/advisory/ADV24199947).
+
+!!! warning "Warning"
+
+ Microsoft strongly recommends not overriding the default behavior that was introduced with the March 2024 security update if there are no functional issues that affect your organization's mail flow.
+
+## Requirements
+
+This script **must** be run as Administrator in `Exchange Management Shell (EMS)`. The user must be a member of the `Organization Management` role group.
+
+## How To Run
+
+### Examples:
+
+This syntax enables processing of `Jpeg` and `AutoCad` file types by the help of the `OutsideInModule` on the server where the command was executed.
+
+```powershell
+.\ConfigureFipFsTextExtractionOverrides.ps1 -ConfigureOverride "Jpeg", "AutoCad" -Action "Allow"
+```
+
+This syntax disables processing of `Jpeg` and `AutoCad` file types by the help of the `OutsideInModule` on the server `ExchangeSrv01` and `ExchangeSrv02`.
+
+```powershell
+.\ConfigureFipFsTextExtractionOverrides.ps1 -ExchangeServerNames ExchangeSrv01, ExchangeSrv02 -ConfigureOverride "Jpeg", "AutoCad" -Action "Block"
+```
+
+This syntax causes Exchange Server to use the previous version of the `OutsideInModule`. The override will be enabled on the system on which the script was executed. Note that this can make your system vulnerable to known vulnerabilities in the previous version and should not be used unless explicitly advised by Microsoft.
+
+```powershell
+.\ConfigureFipFsTextExtractionOverrides.ps1 -ConfigureOverride "OutsideInModule" -Action "Allow"
+```
+
+This syntax disables the override of the version of the `OutsideInModule` module on the server `ExchangeSrv01` and `ExchangeSrv02`.
+
+```powershell
+.\ConfigureFipFsTextExtractionOverrides.ps1 -ExchangeServerNames ExchangeSrv01, ExchangeSrv02 -ConfigureOverride "OutsideInModule" -Action "Block"
+```
+
+This syntax restores the `configuration.xml` from the backup that was created by a previous run of the script on the Exchange server where the script was executed.
+
+```powershell
+.\ConfigureFipFsTextExtractionOverrides.ps1 -Rollback
+```
+
+## Parameters
+
+Parameter | Description
+----------|------------
+ExchangeServerNames | A list of Exchange servers that you want to run the script against.
+SkipExchangeServerNames | A list of Exchange servers that you don't want to execute the configuration action.
+ConfigureOverride | A list of file types that should be allowed to be processed by the `OutsideInModule`. The following input can be used: `XlsbOfficePackage`, `XlsmOfficePackage`, `XlsxOfficePackage`, `ExcelStorage`, `DocmOfficePackage`, `DocxOfficePackage`, `PptmOfficePackage`, `PptxOfficePackage`, `WordStorage`, `PowerPointStorage`, `VisioStorage`, `Rtf`, `Xml`, `OdfTextDocument`, `OdfSpreadsheet`, `OdfPresentation`, `OneNote`, `Pdf`, `Html`, `AutoCad`, `Jpeg`, `Tiff`.
If you want to enable the previous version of the `OutsideInModule` (`8.5.3`) to process file types, you must specify `OutsideInModule` as file type. Note that the `OutsideInModule` value cannot be used together with other file type values.
The input is case-sensitive.
+Action | String parameter to define the action that should be performed. Input can be `Allow` or `Block`. The default value is: `Block`
+Rollback | Switch parameter to restore the `configuration.xml` that was backed-up during a previous run of the script.
+ScriptUpdateOnly | Switch parameter to only update the script without performing any other actions.
+SkipVersionCheck | Switch parameter to skip the automatic version check and script update.
diff --git a/mkdocs.yml b/mkdocs.yml
index 5c19b2d221..f4efb44e36 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -92,6 +92,7 @@ nav:
- CVE-2023-23397:
- Security/CVE-2023-23397/index.md
- FAQ: Security/CVE-2023-23397/FAQ.md
+ - ConfigureFipFsTextExtractionOverrides: Security/ConfigureFipFsTextExtractionOverrides.md
- EOMT: Security/EOMT.md
- EOMTv2: Security/EOMTv2.md
- Extended Protection: Security/Extended-Protection.md