From 6554c55fd50688f24e96ac8dbd9a37297f8dd65b Mon Sep 17 00:00:00 2001 From: David Paulson Date: Mon, 5 Aug 2024 17:52:47 -0500 Subject: [PATCH 1/3] Add InstalledDate for SUs --- .../Invoke-AnalyzerExchangeInformation.ps1 | 9 +++++---- ...voke-AnalyzerSecurityCve-MarchSuSpecial.ps1 | 4 ++-- .../Get-ExchangeInformation.ps1 | 2 +- .../Get-ExchangeUpdates.ps1 | 10 +++++++--- .../Tests/HealthChecker.MockedCalls.Tests.ps1 | 2 +- ...lthCheckerTest.CommonMocks.NotPublished.ps1 | 18 ++++++++++++++---- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index 029691939a..87fdb2de7f 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -129,19 +129,20 @@ function Invoke-AnalyzerExchangeInformation { Add-AnalyzedResultInformation @params } - if ($null -ne $exchangeInformation.BuildInformation.KBsInstalled) { + if ($null -ne $exchangeInformation.BuildInformation.KBsInstalledInfo.PackageName) { Add-AnalyzedResultInformation -Name "Exchange IU or Security Hotfix Detected" @baseParams $problemKbFound = $false $problemKbName = "KB5029388" - foreach ($kb in $exchangeInformation.BuildInformation.KBsInstalled) { + foreach ($kbInfo in $exchangeInformation.BuildInformation.KBsInstalledInfo) { + $kbName = $kbInfo.PackageName $params = $baseParams + @{ - Details = $kb + Details = "$kbName - Installed on $($kbInfo.InstalledDate)" DisplayCustomTabNumber = 2 } Add-AnalyzedResultInformation @params - if ($kb.Contains($problemKbName)) { + if ($kbName.Contains($problemKbName)) { $problemKbFound = $true } } diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1 index 632eb8df0f..8a20634176 100644 --- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-MarchSuSpecial.ps1 @@ -20,8 +20,8 @@ function Invoke-AnalyzerSecurityCve-MarchSuSpecial { #Description: March 2021 Exchange vulnerabilities Security Update (SU) check for outdated version (CUs) #Affected Exchange versions: Exchange 2013, Exchange 2016, Exchange 2016 (we only provide this special SU for these versions) #Fix: Update to a supported CU and apply KB5000871 - $march2021SUInstalled = $null -ne $SecurityObject.ExchangeInformation.BuildInformation.KBsInstalled -and - $SecurityObject.ExchangeInformation.BuildInformation.KBsInstalled -like "*KB5000871*" + $march2021SUInstalled = $null -ne $SecurityObject.ExchangeInformation.BuildInformation.KBsInstalledInfo.PackageName -and + $SecurityObject.ExchangeInformation.BuildInformation.KBsInstalledInfo.PackageName -like "*KB5000871*" $ex2019 = "Exchange2019" $ex2016 = "Exchange2016" $ex2013 = "Exchange2013" diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 index 645e666b88..802f254bda 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 @@ -59,7 +59,7 @@ function Get-ExchangeInformation { CU = $versionInformation.CU ExchangeSetup = $exSetupDetails VersionInformation = $versionInformation - KBsInstalled = [array](Get-ExchangeUpdates -Server $Server -ExchangeMajorVersion $versionInformation.MajorVersion) + KBsInstalledInfo = [array](Get-ExchangeUpdates -Server $Server -ExchangeMajorVersion $versionInformation.MajorVersion) } $dependentServices = (Get-ExchangeDependentServices -MachineName $Server) diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeUpdates.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeUpdates.ps1 index e03b9e1ca3..8f27579a55 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeUpdates.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeUpdates.ps1 @@ -30,14 +30,18 @@ function Get-ExchangeUpdates { $IU = $RegKey.GetSubKeyNames() if ($null -ne $IU) { Write-Verbose "Detected fixes installed on the server" - $fixes = @() + $installedUpdates = New-Object System.Collections.Generic.List[object] foreach ($key in $IU) { $IUKey = $RegKey.OpenSubKey($key) $IUName = $IUKey.GetValue("PackageName") Write-Verbose "Found: $IUName" - $fixes += $IUName + $IUInstalledDate = $IUKey.GetValue("InstalledDate") + $installedUpdates.Add(([PSCustomObject]@{ + PackageName = $IUName + InstalledDate = $IUInstalledDate + })) } - return $fixes + return $installedUpdates } else { Write-Verbose "No IUs found in the registry" } diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1 index 33f59270ef..987e80a677 100644 --- a/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1 +++ b/Diagnostics/HealthChecker/Tests/HealthChecker.MockedCalls.Tests.ps1 @@ -63,6 +63,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 25 + Assert-MockCalled Get-RemoteRegistrySubKey -Exactly 1 Assert-MockCalled Get-NETFrameworkVersion -Exactly 1 Assert-MockCalled Get-DotNetDllFileVersions -Exactly 1 Assert-MockCalled Get-NicPnpCapabilitiesSetting -Exactly 1 @@ -75,7 +76,6 @@ Describe "Testing Health Checker by Mock Data Imports" { Assert-MockCalled Get-AllTlsSettings -Exactly 1 Assert-MockCalled Get-SmbServerConfiguration -Exactly 1 Assert-MockCalled Get-ExchangeAppPoolsInformation -Exactly 1 - Assert-MockCalled Get-ExchangeUpdates -Exactly 1 Assert-MockCalled Get-ExchangeDomainsAclPermissions -Exactly 1 Assert-MockCalled Get-ExchangeAdSchemaClass -Exactly 2 Assert-MockCalled Get-ExchangeServer -Exactly 1 diff --git a/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1 b/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1 index bad0de45c7..2c61effa31 100644 --- a/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1 +++ b/Diagnostics/HealthChecker/Tests/HealthCheckerTest.CommonMocks.NotPublished.ps1 @@ -140,6 +140,20 @@ Mock Get-RemoteRegistryValue { } } +Mock Get-RemoteRegistrySubKey { + param( + [string]$MachineName, + [string]$SubKey + ) + + switch ($SubKey) { + "SOFTWARE\Microsoft\Updates\Exchange 2013" { return $null } + "SOFTWARE\Microsoft\Updates\Exchange 2016" { return $null } + "SOFTWARE\Microsoft\Updates\Exchange 2019" { return $null } + default { throw "Failed to find SubKey: $SubKey" } + } +} + Mock Get-NETFrameworkVersion { return [PSCustomObject]@{ FriendlyName = "4.8" @@ -199,10 +213,6 @@ Mock Get-ExchangeAppPoolsInformation { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetExchangeAppPoolsInformation.xml" } -Mock Get-ExchangeUpdates { - return $null -} - Mock Get-ExchangeAdSchemaClass -ParameterFilter { $SchemaClassName -eq "ms-Exch-Storage-Group" } { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetExchangeAdSchemaClass_ms-Exch-Storage-Group.xml" } From 8ea1d3b53ee4fb0f3356b3dd32b83a67e52a220f Mon Sep 17 00:00:00 2001 From: David Paulson Date: Tue, 6 Aug 2024 09:29:23 -0500 Subject: [PATCH 2/3] Improve pester testing on Exchange Updates --- .../Invoke-AnalyzerExchangeInformation.ps1 | 1 + .../HealthChecker.E19.Scenarios.Tests.ps1 | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index 87fdb2de7f..00366d394d 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -139,6 +139,7 @@ function Invoke-AnalyzerExchangeInformation { $params = $baseParams + @{ Details = "$kbName - Installed on $($kbInfo.InstalledDate)" DisplayCustomTabNumber = 2 + TestingName = "Exchange IU" } Add-AnalyzedResultInformation @params diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1 index bad4fafa83..d59130c0e1 100644 --- a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1 +++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Scenarios.Tests.ps1 @@ -70,6 +70,33 @@ Describe "Testing Health Checker by Mock Data Imports" { if ($Name -eq "MSExchangeMitigation") { return Import-Clixml "$Script:MockDataCollectionRoot\Exchange\GetServiceMitigation.xml" } return Import-Clixml "$Script:MockDataCollectionRoot\OS\GetService1.xml" } + Mock Get-RemoteRegistrySubKey -ParameterFilter { $SubKey -eq "SOFTWARE\Microsoft\Updates\Exchange 2019" } -MockWith { + $obj = New-Object System.Collections.Generic.List[object] + $suObject = [PSCustomObject]@{ + Name = "Empty" + } + $suObject | Add-Member -MemberType ScriptMethod -Name GetSubKeyNames -Value { return @("KB5029388", "KB5030877") } + $suObject | Add-Member -MemberType ScriptMethod -Name OpenSubKey -Value { param($id) if ($id -eq "KB5029388") { + $o = [PSCustomObject]@{ + Name = "Empty" + } + $o | Add-Member -MemberType ScriptMethod -Name GetValue -Value { param ($name) if ($name -eq "PackageName") { + return "Security Update for Exchange Server 2019 Cumulative Update 12 (KB5029388)" + } elseif ( $name -eq "InstalledDate") { return "9/20/2023" } } + return $o + } elseif ($id -eq "KB5030877") { + $o = [PSCustomObject]@{ + Name = "Empty" + } + $o | Add-Member -MemberType ScriptMethod -Name GetValue -Value { param ($name) if ($name -eq "PackageName") { + return "Security Update for Exchange Server 2019 Cumulative Update 12 (KB5030877)" + } elseif ( $name -eq "InstalledDate") { return "10/20/2023" } } + return $o + } + } + $obj.Add($suObject) + return $obj + } SetDefaultRunOfHealthChecker "Debug_Scenario1_Results.xml" } @@ -82,6 +109,8 @@ Describe "Testing Health Checker by Mock Data Imports" { TestObjectMatch "Exchange Server Membership" "Failed" -WriteType "Red" TestObjectMatch "Exchange Trusted Subsystem - Local System Membership" "Exchange Trusted Subsystem - Local System Membership" -WriteType "Red" TestObjectMatch "Exchange Trusted Subsystem - AD Group Membership" "Exchange Trusted Subsystem - AD Group Membership" -WriteType "Red" + $hotfixInstalled = GetObject "Exchange IU" + $hotfixInstalled.Count | Should -Be 2 } It "Dependent Services" { From cfdb3ac710b1fbbf07d08dbf9a8599ae31839742 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Tue, 6 Aug 2024 10:54:32 -0500 Subject: [PATCH 3/3] Add Latest Install Time to Health Checker --- .../Analyzer/Invoke-AnalyzerExchangeInformation.ps1 | 8 ++++++++ Shared/Get-ExSetupFileVersionInfo.ps1 | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index 00366d394d..c6ecf5a667 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -60,6 +60,14 @@ function Invoke-AnalyzerExchangeInformation { } Add-AnalyzedResultInformation @params + if ($null -ne $exchangeInformation.BuildInformation.ExchangeSetup.InstallTime) { + $params = $baseParams + @{ + Name = "Latest Install Time (SU/CU)" + Details = $exchangeInformation.BuildInformation.ExchangeSetup.InstallTime + } + Add-AnalyzedResultInformation @params + } + if ($exchangeInformation.BuildInformation.VersionInformation.Supported -eq $false) { $daysOld = ($date - $exchangeInformation.BuildInformation.VersionInformation.ReleaseDate).Days diff --git a/Shared/Get-ExSetupFileVersionInfo.ps1 b/Shared/Get-ExSetupFileVersionInfo.ps1 index d2ee7d0230..fd6c13928d 100644 --- a/Shared/Get-ExSetupFileVersionInfo.ps1 +++ b/Shared/Get-ExSetupFileVersionInfo.ps1 @@ -15,14 +15,20 @@ function Get-ExSetupFileVersionInfo { $exSetupDetails = [string]::Empty function Get-ExSetupDetailsScriptBlock { try { - Get-Command ExSetup -ErrorAction Stop | ForEach-Object { $_.FileVersionInfo } + $getCommand = Get-Command ExSetup -ErrorAction Stop | ForEach-Object { $_.FileVersionInfo } + $getItem = Get-Item -ErrorAction SilentlyContinue $getCommand[0].FileName + $getCommand | Add-Member -MemberType NoteProperty -Name InstallTime -Value ($getItem.LastAccessTime) + $getCommand } catch { try { Write-Verbose "Failed to find ExSetup by environment path locations. Attempting manual lookup." $installDirectory = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup -ErrorAction Stop).MsiInstallPath if ($null -ne $installDirectory) { - Get-Command ([System.IO.Path]::Combine($installDirectory, "bin\ExSetup.exe")) -ErrorAction Stop | ForEach-Object { $_.FileVersionInfo } + $getCommand = Get-Command ([System.IO.Path]::Combine($installDirectory, "bin\ExSetup.exe")) -ErrorAction Stop | ForEach-Object { $_.FileVersionInfo } + $getItem = Get-Item -ErrorAction SilentlyContinue $getCommand[0].FileName + $getCommand | Add-Member -MemberType NoteProperty -Name InstallTime -Value ($getItem.LastAccessTime) + $getCommand } } catch { Write-Verbose "Failed to find ExSetup, need to fallback."