Private/AzStackHci.Results.Helpers.ps1

# ////////////////////////////////////////////////////////////////////////////
# Function to process results
Function Publish-Results {
    param (
        [array]$results,

        [ValidateSet('HTML', 'CSV')]
        [string]$OutputFormat = 'HTML'
    )

    begin {
        # Write-Debug "Publish-Results: Beginning results processing and publishing"
    }

    process {
        # Assign Row IDs and reorder columns
        $rowIdCounter = 1
        foreach ($result in $script:Results) {
            $result.RowID = $rowIdCounter
            $rowIdCounter++
        }

        [System.Collections.ArrayList]$script:Results = @($script:Results | Select-Object RowID, URL, Port, ArcGateway, IsWildcard, Source, IPAddress, Layer7Status, Layer7Response, Layer7ResponseTime, Note, TCPStatus, CertificateIssuer, CertificateSubject, CertificateThumbprint, IntermediateCertificateIssuer, IntermediateCertificateSubject, IntermediateCertificateThumbprint, RootCertificateIssuer, RootCertificateSubject, RootCertificateThumbprint)

        # Sort the array by the properties Layer7Status (Failed first, then Success, then Skipped), Source, Url
        $statusOrder = @{ 'Failed' = 0; 'Success' = 1; 'Skipped' = 2 }
        [System.Collections.ArrayList]$script:Results = @($script:Results | Sort-Object -Property @{Expression={$statusOrder[$_.Layer7Status]}; Ascending=$true}, Source, Url)

        # Export results based on the OutputFormat parameter (HTML or CSV)
        try {
            switch ($OutputFormat) {
                'HTML' {
                    $htmlStyle = @"
<style>
    body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
    h1 { color: #0078d4; border-bottom: 2px solid #0078d4; padding-bottom: 8px; }
    .summary { background: #fff; padding: 12px 20px; margin-bottom: 20px; border-left: 4px solid #0078d4; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
    .summary li { list-style: none; padding: 2px 0; }
    .summary ul { padding-left: 0; margin: 8px 0; }
    .table-wrapper { overflow-x: auto; max-width: 100%; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
    .top-scroll { overflow-x: auto; max-width: 100%; }
    .top-scroll div { height: 1px; }
    table { border-collapse: collapse; white-space: nowrap; background: #fff; }
    th { background-color: #0078d4; color: #fff; padding: 10px 8px; text-align: left; font-size: 13px; position: sticky; top: 0; }
    td { padding: 8px; border-bottom: 1px solid #e0e0e0; font-size: 13px; }
    tr:nth-child(even) { background-color: #f9f9f9; }
    tr:hover { background-color: #e8f4fd; }
    tr.status-failed { background-color: #fde7e9; }
    tr.status-success { background-color: #e6f4ea; }
    tr.status-skipped { background-color: #fff4ce; }
    h2 { color: #0078d4; margin-top: 24px; }
</style>
"@

                    $preContent = "<h1>Azure Local Connectivity Test Results</h1><!-- SUMMARY -->"
                    $htmlBody = $script:Results | ConvertTo-Html -Title 'Azure Local Connectivity Test Results' -Head $htmlStyle -PreContent $preContent
                    # Color-code rows based on Layer7Status
                    $htmlBody = $htmlBody -replace '<tr><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>Failed</td>', '<tr class="status-failed"><td>$1</td><td>$2</td><td>$3</td><td>$4</td><td>$5</td><td>$6</td><td>$7</td><td>Failed</td>'
                    $htmlBody = $htmlBody -replace '<tr><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>Success</td>', '<tr class="status-success"><td>$1</td><td>$2</td><td>$3</td><td>$4</td><td>$5</td><td>$6</td><td>$7</td><td>Success</td>'
                    $htmlBody = $htmlBody -replace '<tr><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>Skipped</td>', '<tr class="status-skipped"><td>$1</td><td>$2</td><td>$3</td><td>$4</td><td>$5</td><td>$6</td><td>$7</td><td>Skipped</td>'
                    # Bold the Layer7Status cell text
                    $htmlBody = $htmlBody -replace '<td>(Failed|Success|Skipped)</td>', '<td><strong>$1</strong></td>'
                    # Wrap the table in a scrollable div with a synced top scrollbar
                    $htmlBody = $htmlBody -replace '<table>', '<div class="top-scroll" id="topScroll"><div></div></div><div class="table-wrapper" id="bottomScroll"><table>'
                    $htmlBody = $htmlBody -replace '</table>', '</table></div>'
                    # Add JavaScript to sync the top and bottom scrollbars and size the top scroll spacer to match the table width
                    $scrollScript = @"
<script>
document.addEventListener('DOMContentLoaded', function() {
    var top = document.getElementById('topScroll');
    var bottom = document.getElementById('bottomScroll');
    var table = bottom.querySelector('table');
    top.firstElementChild.style.width = table.scrollWidth + 'px';
    top.addEventListener('scroll', function() { bottom.scrollLeft = top.scrollLeft; });
    bottom.addEventListener('scroll', function() { top.scrollLeft = bottom.scrollLeft; });
});
</script>
"@

                    $htmlBody = $htmlBody -replace '</body>', "$scrollScript`n</body>"
                    $htmlBody | Set-Content -Path $script:OutputFile -Encoding UTF8
                }
                'CSV' {
                    $script:Results | Export-Csv -Path $script:OutputFile -NoTypeInformation
                }
            }
        } catch {
            Write-HostAzS "Failed to save test results to $($script:OutputFile)"
            Write-Error "Error: $($_.Exception.Message)"
        }

        # Always generate JSON output file (in addition to HTML/CSV)
        try {
            $script:Results | ConvertTo-Json -Depth 3 -Compress | Set-Content -Path $script:JsonOutputFile -Encoding UTF8
        } catch {
            Write-HostAzS "Failed to save JSON results to $($script:JsonOutputFile)"
            Write-Error "Error: $($_.Exception.Message)"
        }

        # // Calculate the number of successful, failed, and skipped URLs
        # Use TCP Status, if using TCP Connectivity Test switch
        [array]$successResults = @()
        if($IncludeTCPConnectivityTests.IsPresent){
            # Use TCPStatus for successful results
            [array]$successResults = $script:Results | Where-Object { $_.TCPStatus -eq "Success" }
        } else {
            # Otherwise default to Layer7Status
            [array]$successResults = $script:Results | Where-Object { $_.Layer7Status -eq "Success" }
        }
        # Failed URLs results
        [array]$failedResults = @()
        [array]$failedResults = $script:Results | Where-Object { $_.Layer7Status -eq "Failed" }
        # Skipped URLs results
        [array]$skippedResults = @()
        [array]$skippedResults = $script:Results | Where-Object { $_.TCPStatus -like "Skipped*" -or $_.Layer7Status -eq "Skipped" }

        # If the PassThru switch is not present, display the results
        if(-not($PassThru.IsPresent) -and -not($script:SilentMode)){

            if($failedResults.Count -gt 0) {
                Write-HostAzS "`nThe following URLs failed:" -ForegroundColor Red
                if($IncludeTCPConnectivityTests.IsPresent){
                    $failedResults | Format-Table -Property RowID, Source, URL, Port, TCPStatus, IpAddress, Layer7Response -AutoSize
                } else {
                    $failedResults | Format-Table -Property RowID, Source, URL, Port, IpAddress, Layer7Response -AutoSize
                }

            } else {
                Write-HostAzS "`nNo URLs failed.`n" -ForegroundColor Green
            }

            if($successResults.Count -gt 0) {
                Write-HostAzS "The following URLs were successful:" -ForegroundColor Green
                if($IncludeTCPConnectivityTests.IsPresent){
                    $successResults | Format-Table -Property RowID, Source, URL, Port, TCPStatus, IpAddress, Layer7Response -AutoSize
                } else {
                    $successResults | Format-Table -Property RowID, Source, URL, Port, IpAddress, Layer7Response -AutoSize
                }
            } else {
                Write-HostAzS "No URLs were successful.`n"
            }

            if($skippedResults.Count -gt 0) {
                Write-HostAzS "The following URLs were skipped:"
                $skippedResults | Format-Table -Property RowID, Source, URL, Port, Layer7Status, Note -AutoSize
            } else {
                Write-HostAzS "No URLs were skipped.`n" -ForegroundColor Green
            }

            # Display test results summary
            Write-HostAzS "`nTest results summary:"
            Write-HostAzS "---------------------------------`n"

            Write-HostAzS "Total URLs tested: $($script:Results.Count)"
            Write-HostAzS "Successful URLs: $($successResults.Count)" -ForegroundColor Green
            if($failedResults.Count -gt 0){
                Write-HostAzS "Failed URLs: $($failedResults.Count)" -ForegroundColor Red
            } else {
                Write-HostAzS "Failed URLs: $($failedResults.Count)"
            }
            Write-HostAzS "Skipped URLs: $($skippedResults.Count)`n" -ForegroundColor Yellow

            Write-HostAzS "The test result for each endpoint is shown above. For detailed output, including certificate information review the CSV file listed below."
            
        } elseif($PassThru.IsPresent -or $script:SilentMode) {
        
            # If PassThru or NoOutput switches are present, return the results as an array of objects instead of displaying in the console
            return $script:Results
        
        } else {
            # Not expected to hit this else block, but included for safety
        }

        Write-HostAzS "`nIMPORTANT: Only URLs with a Source of 'GitHub', 'Environment Checker' or '<OEM Name> SBE' are required on firewall / proxy outbound allow rules." -ForegroundColor Yellow -NoNewline
        Write-HostAzS " Any URLs with a Source of 'Redirect for ', 'Test for ' are only used for testing connectivity to the required endpoints using the automation in this module." -ForegroundColor Yellow

        Write-HostAzS "`nAzure Local product documentation for firewall requirements can be accessed using this URL from a device with a browser:`n`n`tMicrosoft documentation: 'https://learn.microsoft.com/azure/azure-local/concepts/firewall-requirements'`n" -ForegroundColor Green

    } # End of Process block

    end {
        # Write-Debug "Publish-Results: Results processing and publishing completed"
    }
} # End of Publish-Results


# ////////////////////////////////////////////////////////////////////////////

# Function to remove PII from the transcript file
# ////////////////////////////////////////////////////////////////////////////
Function Remove-PIIFromTranscriptFile {
    <#
    .SYNOPSIS
        Redact the transcript file by removing sensitive information.
    .DESCRIPTION
        This function reads a transcript file, removes sensitive information, and writes the redacted content to a new file.
        The redacted content is saved in the same directory as the original file with "_redacted" appended to the filename.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$TranscriptFilePath
    )

    begin {
        # Write-Debug "Remove-PIIFromTranscriptFile: Beginning PII removal from '$TranscriptFilePath'"
    }

    process {
        # Read the transcript file content
        try {
            $transcriptContent = Get-Content -Path $TranscriptFilePath -ErrorAction SilentlyContinue
        } catch {
            Write-HostAzS "Error: Failed to read the transcript file. $($_.Exception.Message)" -ForegroundColor Red
            Return
        }

        # Check if the transcript file was read successfully
        if($transcriptContent) {
            
            # Set the variable to the original transcript file contents
            $redactedContent = $transcriptContent 
            
            # Redact sensitive information:
            # Update content to replace "Username: <domain>\<username>" with "<REDACTED>"
            $redactedContent = $redactedContent -replace '(?i)(Username: )([a-zA-Z0-9]+\\[a-zA-Z0-9]+)', '$1<REDACTED>'
            # Update content to replace "RunAs User: <domain>\<username>" with "<REDACTED>"
            $redactedContent = $redactedContent -replace '(?i)(RunAs User: )([a-zA-Z0-9]+\\[a-zA-Z0-9]+)', '$1<REDACTED>'

            # Write the redacted content to a new file
            try {
                Set-Content -Path $TranscriptFilePath -Value $redactedContent -ErrorAction SilentlyContinue -Force
                # Check if the file was updated successfully
            } catch {
                Write-HostAzS "Error: Failed to update transcript file." -ForegroundColor Red
            }
        } else {
            Write-HostAzS "Error: Failed to read the transcript file." -ForegroundColor Red
        }
    }

    end {
        # Write-Debug "Remove-PIIFromTranscriptFile: PII removal completed"
    }
} # End Function Remove-PIIFromTranscriptFile