Skip to content

Commit a29b75b

Browse files
authored
Merge pull request #2546 from microsoft/dpaul-HC-HtmlDev
Health Checker: Fix HTML report URL hyperlinks, br tag encoding, and CVE detail row leaks
2 parents 5392ba3 + b6e08f3 commit a29b75b

9 files changed

Lines changed: 195 additions & 9 deletions

Diagnostics/HealthChecker/Analyzer/Add-AnalyzedResultInformation.ps1

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,46 @@ function Add-AnalyzedResultInformation {
5656
begin {
5757
Write-Verbose "Calling $($MyInvocation.MyCommand): $name"
5858

59+
# Extract for Pester Testing - Start
5960
function GetHtmlTextValue {
6061
param(
6162
[string]$OriginalValue
6263
)
6364

64-
# Test for all the changes, if they do not exist, just return now.
65-
if ([string]::IsNullOrEmpty($OriginalValue) -or
66-
($OriginalValue.Contains(">") -eq $false -and
67-
$OriginalValue.Contains("<") -eq $false)) {
65+
if ([string]::IsNullOrEmpty($OriginalValue)) {
6866
return $OriginalValue
6967
}
70-
Write-Verbose "Need to make changes for HTML text"
71-
Write-Verbose "Original Value: $OriginalValue"
72-
$OriginalValue = $OriginalValue.Replace(">", "&gt;")
73-
$OriginalValue = $OriginalValue.Replace("<", "&lt;")
74-
Write-Verbose "New Value: $OriginalValue"
68+
69+
# HTML encode < and > characters so they are not interpreted as HTML tags.
70+
if ($OriginalValue.Contains("<") -or $OriginalValue.Contains(">")) {
71+
Write-Verbose "Need to make changes for HTML text"
72+
Write-Verbose "Original Value: $OriginalValue"
73+
$OriginalValue = $OriginalValue.Replace(">", "&gt;")
74+
$OriginalValue = $OriginalValue.Replace("<", "&lt;")
75+
# Restore intentional <br> tags used for line breaks in multi-value HTML cells.
76+
$OriginalValue = $OriginalValue.Replace("&lt;br&gt;", "<br>")
77+
Write-Verbose "New Value: $OriginalValue"
78+
}
79+
80+
# Convert URLs to clickable hyperlinks in the HTML report.
81+
if ($OriginalValue.Contains("https://") -or $OriginalValue.Contains("http://")) {
82+
$OriginalValue = [regex]::Replace($OriginalValue, '(https?://[^\s<>"''`]+)', {
83+
param($match)
84+
$url = $match.Groups[1].Value
85+
# Strip trailing punctuation that is likely sentence-ending, not part of the URL.
86+
$trailing = ""
87+
while ($url.Length -gt 0 -and $url[-1] -match '[.,;)\]:]') {
88+
$trailing = $url[-1] + $trailing
89+
$url = $url.Substring(0, $url.Length - 1)
90+
}
91+
# cspell:ignore noopener noreferrer
92+
return "<a href=`"$url`" target=`"_blank`" rel=`"noopener noreferrer`">$url</a>$trailing"
93+
})
94+
}
95+
7596
return $OriginalValue
7697
}
98+
# Extract for Pester Testing - End
7799
function GetOutColumnsColorObject {
78100
param(
79101
[object[]]$OutColumns,

Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityADV24199947.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function Invoke-AnalyzerSecurityADV24199947 {
3535
DisplayWriteType = "Red"
3636
Details = "{0}"
3737
DisplayTestingValue = "ADV24199947"
38+
AddHtmlDetailRow = $false
3839
}
3940

4041
if ($SecurityObject.IsEdgeServer) {

Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2022-21978.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function Invoke-AnalyzerSecurityCve-2022-21978 {
4040
Details = $null
4141
DisplayWriteType = $null
4242
DisplayTestingValue = "CVE-2022-21978"
43+
AddHtmlDetailRow = $false
4344
}
4445
if ($null -ne $cveResults -or
4546
$cveResults.Count -gt 0) {

Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2022-41040.NotPublished.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ function Invoke-AnalyzerSecurityCve-2022-41040 {
133133
Details = $details
134134
DisplayWriteType = "Red"
135135
DisplayTestingValue = "CVE-2022-41040"
136+
AddHtmlDetailRow = $false
136137
}
137138
Add-AnalyzedResultInformation @params
138139
}

Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCveAddressedBySerializedDataSigning.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function Invoke-AnalyzerSecurityCveAddressedBySerializedDataSigning {
3232
DisplayGroupingKey = $DisplayGroupingKey
3333
Name = "Security Vulnerability"
3434
DisplayWriteType = "Red"
35+
AddHtmlDetailRow = $false
3536
}
3637

3738
$detailsString = "{0}`r`n`t`tSee: https://portal.msrc.microsoft.com/security-guidance/advisory/{0} for more information."

Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCveAndOverrideCheck.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function Invoke-AnalyzerSecurityCveAndOverrideCheck {
7272
Details = ("{0}$(if($overrideDisabled){" - Disabled By Override"})`r`n`t`tSee: https://portal.msrc.microsoft.com/security-guidance/advisory/{0} for more information." -f $CVEName)
7373
DisplayWriteType = "Red"
7474
DisplayTestingValue = $CVEName
75+
AddHtmlDetailRow = $false
7576
}
7677
Add-AnalyzedResultInformation @params
7778
}

Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityExtendedProtectionConfigState.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function Invoke-AnalyzerSecurityExtendedProtectionConfigState {
6767
TestingName = "Extended Protection Vulnerable"
6868
CustomName = $cveList
6969
DisplayTestingValue = $true
70+
AddHtmlDetailRow = $false
7071
}
7172
$epBasicParams = $baseParams + @{
7273
DisplayWriteType = "Red"
@@ -132,6 +133,7 @@ function Invoke-AnalyzerSecurityExtendedProtectionConfigState {
132133

133134
$epFrontEndParams = $baseParams + @{
134135
Name = "Security Vulnerability"
136+
AddHtmlDetailRow = $false
135137
OutColumns = ([PSCustomObject]@{
136138
DisplayObject = $epFrontEndOutputObjectDisplayValue
137139
ColorizerFunctions = @($epConfig)
@@ -142,6 +144,7 @@ function Invoke-AnalyzerSecurityExtendedProtectionConfigState {
142144

143145
$epBackEndParams = $baseParams + @{
144146
Name = "Security Vulnerability"
147+
AddHtmlDetailRow = $false
145148
OutColumns = ([PSCustomObject]@{
146149
DisplayObject = $epBackEndOutputObjectDisplayValue
147150
ColorizerFunctions = @($epConfig)
@@ -179,6 +182,7 @@ function Invoke-AnalyzerSecurityExtendedProtectionConfigState {
179182
TestingName = "Extended Protection Vulnerable"
180183
CustomName = $cveList
181184
DisplayTestingValue = $true
185+
AddHtmlDetailRow = $false
182186
}
183187
Add-AnalyzedResultInformation @params
184188
} else {

Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,69 @@ Describe "Testing Health Checker by Mock Data Imports" {
1313
. $PSScriptRoot\HealthCheckerTest.CommonMocks.NotPublished.ps1
1414
}
1515

16+
Context "GetHtmlTextValue Unit Tests" {
17+
18+
It "Should return null for null input" {
19+
$result = GetHtmlTextValue -OriginalValue $null
20+
$result | Should -BeNullOrEmpty
21+
}
22+
23+
It "Should return empty string for empty input" {
24+
$result = GetHtmlTextValue -OriginalValue ""
25+
$result | Should -Be ""
26+
}
27+
28+
It "Should return plain text unchanged" {
29+
$result = GetHtmlTextValue -OriginalValue "Exchange 2019 CU11"
30+
$result | Should -Be "Exchange 2019 CU11"
31+
}
32+
33+
It "Should encode angle brackets for certificate SAN values" {
34+
$result = GetHtmlTextValue -OriginalValue "<SAN>CN=mail.contoso.com</SAN>"
35+
$result | Should -Be "&lt;SAN&gt;CN=mail.contoso.com&lt;/SAN&gt;"
36+
}
37+
38+
It "Should encode greater-than sign" {
39+
$result = GetHtmlTextValue -OriginalValue "Value > 100"
40+
$result | Should -Be "Value &gt; 100"
41+
}
42+
43+
It "Should encode less-than sign" {
44+
$result = GetHtmlTextValue -OriginalValue "Value < 100"
45+
$result | Should -Be "Value &lt; 100"
46+
}
47+
48+
It "Should handle mixed content with angle brackets and normal text" {
49+
$result = GetHtmlTextValue -OriginalValue "Status: <Unknown> - Check docs"
50+
$result | Should -Be "Status: &lt;Unknown&gt; - Check docs"
51+
}
52+
53+
It "Should preserve intentional br tags after encoding" {
54+
$testValue = "CVE-2020-1147<br>CVE-2023-36434<br>"
55+
$result = GetHtmlTextValue -OriginalValue $testValue
56+
$result | Should -Be "CVE-2020-1147<br>CVE-2023-36434<br>"
57+
}
58+
59+
It "Should convert URLs to clickable hyperlinks" {
60+
$result = GetHtmlTextValue -OriginalValue "More Information: https://aka.ms/HC-ExBuilds"
61+
# cspell:ignore noopener noreferrer
62+
$result | Should -BeLike '*<a href="https://aka.ms/HC-ExBuilds"*>https://aka.ms/HC-ExBuilds</a>'
63+
}
64+
65+
It "Should convert URLs with trailing sentence punctuation" {
66+
$result = GetHtmlTextValue -OriginalValue "See: https://portal.msrc.microsoft.com/security-guidance/advisory/CVE-2020-1147 for more information."
67+
$result | Should -BeLike '*<a href=*>https://portal.msrc.microsoft.com/security-guidance/advisory/CVE-2020-1147</a> for more information.'
68+
}
69+
70+
It "Should handle br tags combined with URLs in security vulnerability summary" {
71+
$testValue = "CVE-2020-1147`r`n`t`tSee: https://portal.msrc.microsoft.com/security-guidance/advisory/CVE-2020-1147 for more information.<br>"
72+
$result = GetHtmlTextValue -OriginalValue $testValue
73+
$result | Should -BeLike "*<br>*"
74+
$result | Should -Not -BeLike "*&lt;br&gt;*"
75+
$result | Should -BeLike "*<a href=*>*</a>*"
76+
}
77+
}
78+
1679
Context "Basic Exchange 2019 CU11 Testing HyperV" {
1780
BeforeAll {
1881
SetDefaultRunOfHealthChecker "Debug_HyperV_Results.xml"
@@ -192,6 +255,80 @@ Describe "Testing Health Checker by Mock Data Imports" {
192255
$globalRulesWarning = GetObject "Global IIS Rewrite Rules"
193256
$globalRulesWarning | Should -Not -BeNullOrEmpty
194257
}
258+
259+
It "HTML Report - HtmlServerValues Structure" {
260+
$Script:results.HtmlServerValues.ContainsKey("ServerDetails") | Should -Be $true
261+
$Script:results.HtmlServerValues.ContainsKey("OverviewValues") | Should -Be $true
262+
$Script:results.HtmlServerValues["ServerDetails"].Count | Should -BeGreaterThan 0
263+
$Script:results.HtmlServerValues["OverviewValues"].Count | Should -BeGreaterThan 0
264+
265+
$firstDetail = $Script:results.HtmlServerValues["ServerDetails"][0]
266+
$firstDetail.PSObject.Properties.Name | Should -Contain "Name"
267+
$firstDetail.PSObject.Properties.Name | Should -Contain "DetailValue"
268+
$firstDetail.PSObject.Properties.Name | Should -Contain "TableValue"
269+
$firstDetail.PSObject.Properties.Name | Should -Contain "Class"
270+
271+
$firstOverview = $Script:results.HtmlServerValues["OverviewValues"][0]
272+
$firstOverview.PSObject.Properties.Name | Should -Contain "Name"
273+
$firstOverview.PSObject.Properties.Name | Should -Contain "DetailValue"
274+
275+
# ServerDetails captures most entries while OverviewValues is selective
276+
$Script:results.HtmlServerValues["ServerDetails"].Count |
277+
Should -BeGreaterThan $Script:results.HtmlServerValues["OverviewValues"].Count
278+
}
279+
280+
It "HTML Report - Overview Values" {
281+
$serverName = GetHtmlOverviewValue "Server Name"
282+
$serverName | Should -Not -BeNullOrEmpty
283+
$serverName.DetailValue | Should -Not -BeNullOrEmpty
284+
285+
$exchangeVersion = GetHtmlOverviewValue "Exchange Version"
286+
$exchangeVersion | Should -Not -BeNullOrEmpty
287+
$exchangeVersion.DetailValue | Should -Not -BeNullOrEmpty
288+
289+
$generationTime = GetHtmlOverviewValue "Generation Time"
290+
$generationTime | Should -Not -BeNullOrEmpty
291+
292+
$vulnDetected = GetHtmlOverviewValue "Vulnerability Detected"
293+
$vulnDetected | Should -Not -BeNullOrEmpty
294+
if ($vulnDetected.DetailValue -ne "None") {
295+
$vulnDetected.Class | Should -Be "Red"
296+
}
297+
}
298+
299+
It "HTML Report - ServerDetails CSS Class Mapping" {
300+
# Grey write type → empty Class
301+
$serverName = GetHtmlServerDetail "Server Name"
302+
$serverName | Should -Not -BeNullOrEmpty
303+
$serverName.Class | Should -BeNullOrEmpty
304+
305+
# Yellow write type → Yellow Class
306+
$edition = GetHtmlServerDetail "Edition"
307+
if ($null -ne $edition) {
308+
$edition.Class | Should -Be "Yellow"
309+
}
310+
}
311+
312+
It "HTML Report - Security Vulnerabilities HTML Rendering" {
313+
# Individual "Security Vulnerability" entries should NOT appear in ServerDetails.
314+
# They are rolled up into the "Security Vulnerabilities" summary row.
315+
# The regular CVE path sets AddHtmlDetailRow = $false, but the override CVE path
316+
# in Invoke-AnalyzerSecurityCveAndOverrideCheck.ps1 is missing it.
317+
$individualCveEntries = $Script:results.HtmlServerValues["ServerDetails"] |
318+
Where-Object { $_.Name -eq "Security Vulnerability" }
319+
$individualCveEntries | Should -BeNullOrEmpty
320+
321+
# The summary row should be present
322+
$entry = GetHtmlServerDetail "Security Vulnerabilities"
323+
$entry | Should -Not -BeNullOrEmpty
324+
325+
if ($null -ne $entry -and -not [string]::IsNullOrEmpty($entry.DetailValue)) {
326+
$entry.DetailValue | Should -BeLike "*CVE-*"
327+
# Validates the fix for the PR #2475 regression: <br> tags must be preserved
328+
$entry.DetailValue | Should -Not -BeLike "*&lt;br&gt;*"
329+
$entry.DetailValue | Should -BeLike "*<br>*"
330+
}
331+
}
195332
}
196333

197334
Context "Basic Exchange 2019 CU11 Testing Physical" {

Diagnostics/HealthChecker/Tests/HealthCheckerTests.ImportCode.NotPublished.ps1

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,24 @@ function TestObjectMatch {
9191
Should -Be $WriteType
9292
}
9393

94+
function GetHtmlServerDetail {
95+
[CmdletBinding()]
96+
param(
97+
[Parameter(Mandatory = $true, Position = 1)]
98+
[string]$Name
99+
)
100+
$Script:results.HtmlServerValues["ServerDetails"] | Where-Object { $_.Name -eq $Name }
101+
}
102+
103+
function GetHtmlOverviewValue {
104+
[CmdletBinding()]
105+
param(
106+
[Parameter(Mandatory = $true, Position = 1)]
107+
[string]$Name
108+
)
109+
$Script:results.HtmlServerValues["OverviewValues"] | Where-Object { $_.Name -eq $Name }
110+
}
111+
94112
function TestOutColumnObjectCompare {
95113
[CmdletBinding()]
96114
param(

0 commit comments

Comments
 (0)