tests/Test-Assessment.27003.ps1
|
<#
.SYNOPSIS TLS inspection failure rate remains below 1% to ensure consistent traffic visibility. .DESCRIPTION Queries a Log Analytics workspace for NetworkAccessTraffic logs over the last 7 days and calculates the TLS inspection failure rate for intercepted traffic. A failure rate of 1% or higher indicates systemic issues that are creating blind spots in encrypted-traffic visibility. .NOTES Test ID: 27003 Category: Global Secure Access Required APIs: - Microsoft Graph beta: networkAccess/tlsInspectionPolicies (prerequisite check) - Azure Monitor diagnostic settings: providers/microsoft.aadiam/diagnosticsettings - Log Analytics query: {workspaceResourceId}/query #> function Test-Assessment-27003 { [ZtTest( Category = 'Global Secure Access', ImplementationCost = 'Medium', MinimumLicense = ('Entra_Premium_Internet_Access'), Pillar = 'Network', RiskLevel = 'High', SfiPillar = 'Protect networks', TenantType = ('Workforce'), TestId = 27003, Title = 'TLS inspection failure rate remains below 1% to ensure consistent traffic visibility', UserImpact = 'Medium' )] [CmdletBinding()] param() #region Helper Functions function Invoke-LogAnalyticsQuery { param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Query ) $body = @{ query = $Query } | ConvertTo-Json $response = Invoke-ZtAzureRequest -Path $Path -Method POST -Payload $body -FullResponse if ($response.StatusCode -ge 400) { try { $errorBody = $response.Content | ConvertFrom-Json -ErrorAction Stop $errorMsg = if ($errorBody.error) { $errorBody.error.message } else { "HTTP $($response.StatusCode)" } } catch { $errorMsg = "HTTP $($response.StatusCode)" } throw "Log Analytics query failed: $errorMsg" } $parsed = $response.Content | ConvertFrom-Json -ErrorAction Stop $table = $parsed.Tables[0] # Convert columnar response to PSCustomObjects $results = @() foreach ($row in $table.Rows) { $obj = [ordered]@{} for ($i = 0; $i -lt $table.Columns.Count; $i++) { $obj[$table.Columns[$i].Name] = $row[$i] } $results += [PSCustomObject]$obj } return $results } #endregion Helper Functions #region Data Collection Write-PSFMessage '🟦 Start TLS inspection failure rate evaluation' -Tag Test -Level VeryVerbose $activity = 'Checking TLS inspection failure rate' Write-ZtProgress -Activity $activity -Status 'Checking connections' # Check Azure connection and cloud environment first to avoid unnecessary API calls # in sovereign clouds (this test is only applicable to the Global/AzureCloud environment) Write-ZtProgress -Activity $activity -Status 'Checking Azure connection' $azContext = Get-AzContext -ErrorAction SilentlyContinue if (-not $azContext) { Write-PSFMessage 'Not connected to Azure.' -Tag Test -Level Warning Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure return } if ($azContext.Environment.Name -ne 'AzureCloud') { Write-PSFMessage 'This test is only applicable to the AzureCloud environment.' -Tag Test -Level VeryVerbose Add-ZtTestResultDetail -SkippedBecause NotSupported return } # Prerequisite: TLS inspection must be configured Write-ZtProgress -Activity $activity -Status 'Checking TLS inspection policies' try { $tlsInspectionPolicies = Invoke-ZtGraphRequest -RelativeUri 'networkAccess/tlsInspectionPolicies' -ApiVersion beta } catch { if ($_.Exception.Message -match '403|Forbidden') { Write-PSFMessage 'Access denied to networkAccess/tlsInspectionPolicies. The tenant may not be licensed for Global Secure Access or the app is missing required permissions.' -Tag Test -Level Warning Add-ZtTestResultDetail -SkippedBecause NotSupported return } throw } if (-not $tlsInspectionPolicies -or $tlsInspectionPolicies.Count -eq 0) { Write-PSFMessage 'No TLS inspection policies configured.' -Tag Test -Level VeryVerbose Add-ZtTestResultDetail -SkippedBecause NotSupported -Result "TLS inspection is not configured in this tenant. This check is not applicable until a TLS inspection policy is created." return } Write-PSFMessage "Found $($tlsInspectionPolicies.Count) TLS inspection policy/policies." -Tag Test -Level VeryVerbose # Find Log Analytics workspace from Entra diagnostic settings Write-ZtProgress -Activity $activity -Status 'Querying diagnostic settings for Log Analytics workspace' try { $diagResult = Invoke-ZtAzureRequest -Path '/providers/microsoft.aadiam/diagnosticsettings?api-version=2017-04-01-preview' -FullResponse if ($diagResult.StatusCode -eq 403) { Write-PSFMessage 'The signed-in user does not have access to check diagnostic settings.' -Tag Test -Level Verbose Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } if ($diagResult.StatusCode -ge 400) { throw "Diagnostic settings request failed with status code $($diagResult.StatusCode)" } } catch { throw } $diagnosticSettings = ($diagResult.Content | ConvertFrom-Json -ErrorAction Stop).value # Find a workspace that has NetworkAccessTrafficLogs enabled $workspaceResourceId = $null $matchedSettingName = $null foreach ($setting in $diagnosticSettings) { $wsId = $setting.properties.workspaceId if (-not [string]::IsNullOrEmpty($wsId)) { $enabledLogs = $setting.properties.logs | Where-Object { $_.enabled -eq $true } | Select-Object -ExpandProperty category if ($enabledLogs -contains 'NetworkAccessTrafficLogs') { $workspaceResourceId = $wsId $matchedSettingName = $setting.name break } } } if (-not $workspaceResourceId) { Write-PSFMessage 'No diagnostic setting exports NetworkAccessTrafficLogs to a Log Analytics workspace.' -Tag Test -Level Warning Add-ZtTestResultDetail -SkippedBecause NotSupported -Result "No diagnostic setting is configured to export NetworkAccessTrafficLogs to a Log Analytics workspace. Configure diagnostic settings to enable this check." return } Write-PSFMessage "Using workspace from diagnostic setting '$matchedSettingName': $workspaceResourceId" -Tag Test -Level VeryVerbose $logAnalyticsQueryUri = "$workspaceResourceId/query?api-version=2017-10-01" # Q1: Calculate TLS inspection failure rate over the last 7 days Write-ZtProgress -Activity $activity -Status 'Querying TLS inspection failure rate (last 7 days)' $q1Kql = @" NetworkAccessTraffic | where TimeGenerated >= ago(7d) | where TlsAction == "Intercepted" | summarize TotalIntercepted = count(), FailureCount = countif(TlsStatus == "Failure"), SuccessCount = countif(TlsStatus == "Success") | extend FailureRate = round(todouble(FailureCount) / todouble(TotalIntercepted) * 100, 2) | project TotalIntercepted, SuccessCount, FailureCount, FailureRate "@ try { $q1Results = @(Invoke-LogAnalyticsQuery -Path $logAnalyticsQueryUri -Query $q1Kql) } catch { Write-PSFMessage "Failed to query Log Analytics: $_" -Tag Test -Level Warning Add-ZtTestResultDetail -SkippedBecause NotSupported -Result "Unable to query Log Analytics workspace. Ensure the signed-in user has Log Analytics Reader access. Error: $_" return } # Extract Q1 metrics $totalIntercepted = 0 $successCount = 0 $failureCount = 0 $failureRate = 0.0 if ($q1Results.Count -gt 0) { $totalIntercepted = [long]$q1Results[0].TotalIntercepted $successCount = [long]$q1Results[0].SuccessCount $failureCount = [long]$q1Results[0].FailureCount # Handle NaN (when TotalIntercepted is 0, KQL returns NaN) $rawRate = $q1Results[0].FailureRate if ($rawRate -is [string] -and $rawRate -eq 'NaN') { $failureRate = 0.0 } elseif ($null -ne $rawRate) { $failureRate = [double]$rawRate } } Write-PSFMessage "Q1 results: Total=$totalIntercepted, Success=$successCount, Failure=$failureCount, Rate=$failureRate%" -Tag Test -Level VeryVerbose # Q2: Top failure destinations (only query if there are failures) $q2Results = @() if ($failureCount -gt 0) { Write-ZtProgress -Activity $activity -Status 'Querying top failure destinations' $q2Kql = @" NetworkAccessTraffic | where TimeGenerated >= ago(7d) | where TlsAction == "Intercepted" and TlsStatus == "Failure" | summarize FailureCount = count() by DestinationFqdn | top 20 by FailureCount desc | project DestinationFqdn, FailureCount "@ try { $q2Results = @(Invoke-LogAnalyticsQuery -Path $logAnalyticsQueryUri -Query $q2Kql) } catch { Write-PSFMessage "Failed to query top failure destinations: $_" -Tag Test -Level Warning } } # Q3: Daily trend analysis $q3Results = @() if ($totalIntercepted -gt 0) { Write-ZtProgress -Activity $activity -Status 'Querying daily failure rate trend' $q3Kql = @" NetworkAccessTraffic | where TimeGenerated >= ago(7d) | where TlsAction == "Intercepted" | summarize TotalIntercepted = count(), FailureCount = countif(TlsStatus == "Failure") by bin(TimeGenerated, 1d) | extend FailureRate = round(todouble(FailureCount) / todouble(TotalIntercepted) * 100, 2) | project Date = format_datetime(TimeGenerated, 'yyyy-MM-dd'), TotalIntercepted, FailureCount, FailureRate | order by Date asc "@ try { $q3Results = @(Invoke-LogAnalyticsQuery -Path $logAnalyticsQueryUri -Query $q3Kql) } catch { Write-PSFMessage "Failed to query daily trend: $_" -Tag Test -Level Warning } } #endregion Data Collection #region Assessment Logic $passed = $false $testResultMarkdown = '' if ($totalIntercepted -eq 0) { # No intercepted traffic — TLS inspection not actively in use $passed = $true $testResultMarkdown = "✅ No TLS-intercepted traffic was found in the last 7 days. TLS inspection policies exist but no traffic was intercepted during the evaluation period.`n`n%TestResult%" } elseif ($failureRate -ge 1) { # Fail: failure rate is 1% or higher $testResultMarkdown = "❌ TLS inspection failure rate exceeds 1% threshold ($failureRate%). Investigate failing destinations and consider adding bypass rules for incompatible applications or resolving certificate trust issues.`n`n%TestResult%" } else { # Pass: failure rate below 1% $passed = $true $testResultMarkdown = "✅ TLS inspection failure rate is below 1% ($failureRate%) over the last 7 days, indicating healthy encrypted traffic visibility.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation $tlsInspectionLink = 'https://entra.microsoft.com/#view/Microsoft_Azure_Network_Access/TLSInspectionPolicy.ReactView' $workspaceName = ($workspaceResourceId -split '/')[-1] $workspacePortalLink = "https://portal.azure.com/#resource${workspaceResourceId}/overview" # Build health summary section $statusText = if ($passed) { 'Pass' } else { 'Fail' } $mdInfo = @" ## [TLS inspection health summary]($tlsInspectionLink) | Metric | Value | | :--- | :--- | | Evaluation period | Last 7 days | | Log Analytics workspace | [$(Get-SafeMarkdown $workspaceName)]($workspacePortalLink) | | Total intercepted transactions | $totalIntercepted | | Successful inspections | $successCount | | Failed inspections | $failureCount | | Failure rate | $failureRate% | | Status | $statusText | "@ # Top failing destinations (only if failures exist) if ($q2Results.Count -gt 0) { $destRows = '' foreach ($dest in $q2Results) { $destRows += "| $(Get-SafeMarkdown $dest.DestinationFqdn) | $($dest.FailureCount) |`n" } $mdInfo += @" ## Top failing destinations | Destination FQDN | Failure count | | :--- | :--- | $destRows "@ } # Daily trend (only if there was intercepted traffic) if ($q3Results.Count -gt 0) { $trendRows = '' foreach ($day in $q3Results) { $dayRate = $day.FailureRate if ($dayRate -is [string] -and $dayRate -eq 'NaN') { $dayRate = '0' } $trendRows += "| $($day.Date) | $($day.TotalIntercepted) | $($day.FailureCount) | $dayRate% |`n" } $mdInfo += @" ## Daily trend | Date | Total intercepted | Failures | Failure rate | | :--- | :--- | :--- | :--- | $trendRows "@ } $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '27003' Title = 'TLS inspection failure rate remains below 1% to ensure consistent traffic visibility' Status = $passed Result = $testResultMarkdown } Add-ZtTestResultDetail @params } |