diff --git a/Diagnostics/HealthChecker/Analyzer/Get-URLRewriteRule.ps1 b/Diagnostics/HealthChecker/Analyzer/Get-URLRewriteRule.ps1 index 1cb97c214e..a0d9bdaf82 100644 --- a/Diagnostics/HealthChecker/Analyzer/Get-URLRewriteRule.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Get-URLRewriteRule.ps1 @@ -37,7 +37,21 @@ function Get-URLRewriteRule { $appHostConfigLocations = $ApplicationHostConfig.configuration.Location.path } process { - foreach ($key in $WebConfigContent.Keys) { + # Build combined location list: WebConfigContent keys + appHost-only locations. + # Some IIS locations (e.g., EAS/Proxy) exist only in applicationHost.config and have + # no web.config entry from Get-WebApplication. We still need to walk up inheritance for them. + $allLocations = [System.Collections.Generic.List[string]]::new() + foreach ($wcKey in $WebConfigContent.Keys) { + $allLocations.Add($wcKey) + } + foreach ($appHostPath in $appHostConfigLocations) { + if (-not [string]::IsNullOrEmpty($appHostPath) -and + -not $WebConfigContent.ContainsKey($appHostPath)) { + $allLocations.Add($appHostPath) + } + } + + foreach ($key in $allLocations) { Write-Verbose "Working on key: $key" $continue = $true $clearInbound = $false diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 index e0e1cc14c7..cb0b208364 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 @@ -612,6 +612,14 @@ function Invoke-AnalyzerIISInformation { $currentSection = $urlRewriteRules[$key] if ($currentSection.Count -ne 0) { + # Collect entries so inherited rules that are removed at a lower level are excluded. + $excludeRules = @() + foreach ($section in $currentSection) { + if ($null -ne $section.Remove) { + $excludeRules += $section.Remove.Name + } + } + foreach ($rule in $currentSection.rule) { if ($null -eq $rule) { @@ -621,12 +629,24 @@ function Invoke-AnalyzerIISInformation { # skip over disabled rules. Write-Verbose "skipping over disabled rule: $($rule.Name) for vDir '$key'" continue + } elseif ($rule.Name -in $excludeRules) { + Write-Verbose "skipping removed rule: $($rule.Name) for vDir '$key'" + continue } #multiple match type possibilities, but should only be one per rule. - $propertyType = ($rule.match | Get-Member | Where-Object { $_.MemberType -eq "Property" }).Name - $isUrlMatchProblem = $propertyType -eq "url" -and $rule.match.$propertyType -eq "*" - $matchProperty = "$propertyType - $($rule.match.$propertyType)" + $allProperties = @(($rule.match | Get-Member | Where-Object { $_.MemberType -eq "Property" }).Name) + # When has extra attributes (negate, ignoreCase), Get-Member returns multiple properties. + # Filter to the actual match target property for display and the URL Match Problem check. + $propertyType = ($allProperties | Where-Object { $_ -eq "url" -or $_ -eq "serverVariable" } | Select-Object -First 1) + + if ($null -eq $propertyType) { + $propertyType = $allProperties | Select-Object -First 1 + } + + $matchValue = $rule.match.$propertyType + $isUrlMatchProblem = $propertyType -eq "url" -and $matchValue -eq "*" + $matchProperty = "$propertyType - $matchValue" $displayObject = [PSCustomObject]@{ RewriteRuleName = $rule.name @@ -682,6 +702,13 @@ function Invoke-AnalyzerIISInformation { $currentSection = $urlOutboundRewriteRules[$key] if ($currentSection.Count -ne 0) { + $excludeOutboundRules = @() + foreach ($section in $currentSection) { + if ($null -ne $section.Remove) { + $excludeOutboundRules += $section.Remove.Name + } + } + foreach ($rule in $currentSection.rule) { if ($null -eq $rule) { @@ -690,6 +717,9 @@ function Invoke-AnalyzerIISInformation { } elseif ($rule.enabled -eq "false") { Write-Verbose "skipping over disabled outbound rule: $($rule.Name) for vDir '$key'" continue + } elseif ($rule.Name -in $excludeOutboundRules) { + Write-Verbose "skipping removed outbound rule: $($rule.Name) for vDir '$key'" + continue } $displayObject = [PSCustomObject]@{ diff --git a/Diagnostics/HealthChecker/Analyzer/Tests/Get-URLRewriteRule.Tests.ps1 b/Diagnostics/HealthChecker/Analyzer/Tests/Get-URLRewriteRule.Tests.ps1 index 0029955fed..90ce88ee86 100644 --- a/Diagnostics/HealthChecker/Analyzer/Tests/Get-URLRewriteRule.Tests.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Tests/Get-URLRewriteRule.Tests.ps1 @@ -96,6 +96,32 @@ Describe "Get-URLRewriteRule" { } } + Context "AppHost-only locations (no web.config entry)" { + + It "Should process Proxy location that only exists in appHost" { + # EAS/Proxy exists in applicationHost.config but is not returned by Get-WebApplication + # so it has no entry in $webConfigContent. The fix adds it from appHost locations. + $Script:result.Inbound.ContainsKey("Default Web Site/Microsoft-Server-ActiveSync/Proxy") | Should -Be $true + $Script:result.Outbound.ContainsKey("Default Web Site/Microsoft-Server-ActiveSync/Proxy") | Should -Be $true + } + + It "Should inherit parent rules for appHost-only Proxy location" { + # Proxy has no rules at its own appHost level. Walk-up should reach DWS web.config + # (CVE-2022-41040) and DWS appHost (disabled HTTPS redirect, AppHost Only Rule) and global. + $proxyRules = $Script:result.Inbound["Default Web Site/Microsoft-Server-ActiveSync/Proxy"] + $proxyRuleNames = @($proxyRules.rule.name | Where-Object { $null -ne $_ }) + $proxyRuleNames | Should -Contain "CVE-2022-41040 Mitigation" + $proxyRuleNames | Should -Contain "Global Block Bad User Agents" + } + + It "Should not duplicate keys that exist in both WebConfigContent and appHost" { + # "Default Web Site" exists in both $webConfigContent and appHost locations. + # It should appear exactly once in the result, not duplicated. + $dwsCount = @($Script:result.Inbound.Keys | Where-Object { $_ -eq "Default Web Site" }).Count + $dwsCount | Should -Be 1 + } + } + Context "Clear stops inheritance" { It "Should stop at clear in appHost location for Default Web Site/mapi" { diff --git a/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/DefaultWebSite_web.config b/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/DefaultWebSite_web.config index 7d1d3b446f..40ac5b0c4f 100644 --- a/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/DefaultWebSite_web.config +++ b/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/DefaultWebSite_web.config @@ -43,6 +43,7 @@ + diff --git a/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/applicationHost.config b/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/applicationHost.config index 27a318bcc9..84ec5b5f56 100644 --- a/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/applicationHost.config +++ b/Diagnostics/HealthChecker/Tests/DataCollection/E19/Exchange/IIS/applicationHost.config @@ -1334,6 +1334,14 @@ + + + + + + + + diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 index 4ad67cffc8..7c14b9c0ab 100644 --- a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 +++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 @@ -239,10 +239,18 @@ Describe "Testing Health Checker by Mock Data Imports" { # Verify inbound URL rewrite rules are displayed (deduplicated across vDirs) $inboundRules = GetObject "Inbound URL Rewrite Rules" - $inboundRules.Count | Should -Be 2 + $inboundRules.Count | Should -Be 3 $inboundRuleNames = $inboundRules.RewriteRuleName.Value $inboundRuleNames | Should -Contain "CVE-2022-41040 Mitigation" $inboundRuleNames | Should -Contain "Global Block Bad User Agents" + $inboundRuleNames | Should -Contain "Negate Match Test Rule" + + # Verify rules excluded by in DWS web.config do not appear in detailed display + $inboundRuleNames | Should -Not -Contain "AppHost Only Rule" + + # Verify match property resolves correctly when has extra attributes like negate + $negateRule = $inboundRules | Where-Object { $_.RewriteRuleName.Value -eq "Negate Match Test Rule" } + $negateRule.MatchProperty.Value | Should -Be "url - .*" # Verify outbound URL rewrite rules are displayed (deduplicated across vDirs) $outboundRules = GetObject "Outbound URL Rewrite Rules"