From 1515da37d2703bdd8272a1e862bddee11b844aa4 Mon Sep 17 00:00:00 2001 From: Komal Date: Fri, 23 Jan 2026 18:25:04 +0530 Subject: [PATCH 01/37] add 25539 --- src/powershell/tests/Test-Assessment.25539.md | 12 ++ .../tests/Test-Assessment.25539.ps1 | 182 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.25539.md create mode 100644 src/powershell/tests/Test-Assessment.25539.ps1 diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md new file mode 100644 index 000000000..a880f21b0 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -0,0 +1,12 @@ +Azure Firewall Premium offers signature-based IDPS to quickly detect attacks by identifying specific patterns, such as byte sequences in network traffic or known malicious instruction sequences used by malware. These IDPS signatures apply to both application and network-level traffic (Layers 3-7). They are fully managed and continuously updated. IDPS can be applied to inbound, spoke-to-spoke (East-West), and outbound traffic, including traffic to/from an on-premises network. + +This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. + +If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. + +**Remediation action** +Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. + +- [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 new file mode 100644 index 000000000..302f04b24 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -0,0 +1,182 @@ +<# +.SYNOPSIS + Validates Intrusion Detection is Enabled in Deny Mode on Azure Firewall. +.DESCRIPTION + This test validates that Azure Firewall Policies have Intrusion Detection enabled in Deny mode. + Checks all firewall policies in the subscription and reports their intrusion detection status. +.NOTES + Test ID: 25539 + Category: Azure Network Security + Required API: Azure Firewall Policies +#> + +function Test-Assessment-25539 { + [ZtTest( + Category = 'Azure Network Security', + ImplementationCost = 'Low', + MinimumLicense = ('Azure_Firewall_Premium'), + Pillar = 'Network', + RiskLevel = 'High', + SfiPillar = 'Protect networks', + TenantType = ('Workforce'), + TestId = 25539, + Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + #region Data Collection + $activity = 'Azure Firewall Intrusion Detection' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Enumerating Firewall Policies' + + # Query subscriptions using REST API + $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') + $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2020-01-01" + + try { + $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop + } + catch { + if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + } + + $subscriptions = ($subscriptionsResponse.Content | ConvertFrom-Json).value + + if (-not $subscriptions) { + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + + $results = @() + + foreach ($sub in $subscriptions) { + + Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction SilentlyContinue | Out-Null + + # Query Azure Firewall Policies + try { + $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2023-04-01" + Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + + $policyResponse = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content | ConvertFrom-Json + $policies = $policyResponse.value + + } + catch { + Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + continue + } + + if (-not $policies) { continue } + + # Check intrusion detection mode for each firewall policy + foreach ($policyResource in $policies) { + + # Skip if policy is missing required properties + if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { + Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + continue + } + + # Skip if SKU tier is not Premium + if ($policyResource.Properties.sku.tier -ne 'Premium') { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + continue + } + + $idMode = $policyResource.Properties.intrusionDetection.mode + + # Skip if intrusion detection mode is not configured + if ([string]::IsNullOrEmpty($idMode)) { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose + continue + } + + # Map intrusion detection mode to user-friendly display values + $detectionModeDisplay = switch ($idMode) { + 'Deny' { 'Alert and Deny' } + 'Alert' { 'Alert Only' } + 'Off' { 'Disabled' } + default { $idMode } + } + + $subContext = Get-AzContext + + $results += [PSCustomObject]@{ + PolicyName = $policyResource.Name + SubscriptionName = $subContext.Subscription.Name + SubscriptionId = $subContext.Subscription.Id + IntrusionDetectionMode = $detectionModeDisplay + PolicyID = $policyResource.Id + Passed = $idMode -eq 'Deny' + } + } + } + #endregion Data Collection + + #region Assessment Logic + if (-not $results) { + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + + $passed = ($results | Where-Object { -not $_.Passed }).Count -eq 0 + + if ($passed) { + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for all Azure Firewall policies.`n`n%TestResult%" + } + else { + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for all Azure Firewall policies.`n`n%TestResult%" + } + #endregion Assessment Logic + + #region Report Generation + $reportTitle = "Firewall policies" + $tableRows = "" + + if ($results.Count -gt 0) { + # Create a here-string with format placeholders {0}, {1}, etc. + $formatTemplate = @' + +## {0} + +| Policy name | Subscription name | IDPS Inspection mode | Result | +| :--- | :--- | :--- | :--- | +{1} + +'@ + + foreach ($item in $results | Sort-Object PolicyName) { + $policyLink = "https://portal.azure.com/#resource$($item.PolicyID)" + $subLink = "https://portal.azure.com/#resource/subscriptions/$($item.SubscriptionId)" + $policyMd = "[$(Get-SafeMarkdown -Text $item.PolicyName)]($policyLink)" + $subMd = "[$(Get-SafeMarkdown -Text $item.SubscriptionName)]($subLink)" + $icon = if ($item.Passed) { '✅' } else { '❌' } + $tableRows += "| $policyMd | $subMd | $($item.IntrusionDetectionMode) | $icon |`n" + } + + # Format the template by replacing placeholders with values + $mdInfo = $formatTemplate -f $reportTitle, $tableRows + } + + # Replace the placeholder with the detailed information + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '25539' + Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall' + Status = $passed + Result = $testResultMarkdown + } + + Add-ZtTestResultDetail @params +} From 7f6248fbf8cd3ec09ec0ea942d23574e1967110a Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 29 Jan 2026 12:45:18 +0530 Subject: [PATCH 02/37] Add test 25539 --- src/powershell/tests/Test-Assessment.25539.md | 4 +- .../tests/Test-Assessment.25539.ps1 | 81 +++++++++++++------ 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md index a880f21b0..2cdeeb8a1 100644 --- a/src/powershell/tests/Test-Assessment.25539.md +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -1,11 +1,11 @@ Azure Firewall Premium offers signature-based IDPS to quickly detect attacks by identifying specific patterns, such as byte sequences in network traffic or known malicious instruction sequences used by malware. These IDPS signatures apply to both application and network-level traffic (Layers 3-7). They are fully managed and continuously updated. IDPS can be applied to inbound, spoke-to-spoke (East-West), and outbound traffic, including traffic to/from an on-premises network. -This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. +This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. **Remediation action** -Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. +Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. - [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 302f04b24..4e8d5351c 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -26,69 +26,101 @@ function Test-Assessment-25539 { [CmdletBinding()] param() - Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + #Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose #region Data Collection $activity = 'Azure Firewall Intrusion Detection' - Write-ZtProgress ` - -Activity $activity ` - -Status 'Enumerating Firewall Policies' + #Write-ZtProgress ` + #-Activity $activity ` + #-Status 'Enumerating Firewall Policies' # Query subscriptions using REST API $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') - $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2020-01-01" + $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2025-03-01" try { $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop + $subscriptionsContent = $subscriptionsResponse.Content + + if (-not $subscriptionsContent) { + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + + $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value } catch { if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { - Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + # Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } - } - - $subscriptions = ($subscriptionsResponse.Content | ConvertFrom-Json).value - - if (-not $subscriptions) { - Add-ZtTestResultDetail -SkippedBecause NoResults + #Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning return } $results = @() foreach ($sub in $subscriptions) { - Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction SilentlyContinue | Out-Null # Query Azure Firewall Policies try { - $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2023-04-01" - Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01" + #Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" - $policyResponse = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content | ConvertFrom-Json - $policies = $policyResponse.value + $policyResponseContent = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content + + if (-not $policyResponseContent) { + #Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning + continue + } + $policyResponse = $policyResponseContent | ConvertFrom-Json + $policies = $policyResponse.value } catch { - Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { + #Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } + #Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning continue } if (-not $policies) { continue } - # Check intrusion detection mode for each firewall policy + # Step 3: Get individual firewall policy details + $detailedPolicies = @() foreach ($policyResource in $policies) { + try { + $detailUri = "$resourceManagerUrl$($policyResource.id)?api-version=2025-03-01" + $detailResponseContent = (Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop).Content + + if (-not $detailResponseContent) { + #Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning + continue + } + + $detailResponse = $detailResponseContent | ConvertFrom-Json + $detailedPolicies += $detailResponse + } + catch { + #Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + } + } + + # Check intrusion detection mode for each firewall policy + foreach ($policyResource in $detailedPolicies) { # Skip if policy is missing required properties if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { - Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + #Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose continue } # Skip if SKU tier is not Premium if ($policyResource.Properties.sku.tier -ne 'Premium') { - Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose continue } @@ -96,7 +128,7 @@ function Test-Assessment-25539 { # Skip if intrusion detection mode is not configured if ([string]::IsNullOrEmpty($idMode)) { - Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose + #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose continue } @@ -124,11 +156,12 @@ function Test-Assessment-25539 { #region Assessment Logic if (-not $results) { - Add-ZtTestResultDetail -SkippedBecause NoResults + Add-ZtTestResultDetail -SkippedBecause NotSupported return } - $passed = ($results | Where-Object { -not $_.Passed }).Count -eq 0 + $failedPolicies = @($results | Where-Object { -not $_.Passed }) + $passed = $failedPolicies.Count -eq 0 if ($passed) { $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for all Azure Firewall policies.`n`n%TestResult%" From b4e0d1401632e6d5df7bc747fc74b89534d0501a Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 29 Jan 2026 13:19:50 +0530 Subject: [PATCH 03/37] updated user facing message --- .../tests/Test-Assessment.25539.ps1 | 84 +++++++++++-------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 4e8d5351c..15075a4af 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -30,9 +30,9 @@ function Test-Assessment-25539 { #region Data Collection $activity = 'Azure Firewall Intrusion Detection' - #Write-ZtProgress ` - #-Activity $activity ` - #-Status 'Enumerating Firewall Policies' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Enumerating Firewall Policies' # Query subscriptions using REST API $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') @@ -40,8 +40,18 @@ function Test-Assessment-25539 { try { $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop - $subscriptionsContent = $subscriptionsResponse.Content + if ($subscriptionsResponse.StatusCode -eq 403) { + Write-PSFMessage 'The signed in user does not have access to check subscriptions.' -Level Verbose + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + + if ($subscriptionsResponse.StatusCode -ge 400) { + throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" + } + + $subscriptionsContent = $subscriptionsResponse.Content if (-not $subscriptionsContent) { Add-ZtTestResultDetail -SkippedBecause NoResults return @@ -50,11 +60,7 @@ function Test-Assessment-25539 { $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value } catch { - if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { - # Add-ZtTestResultDetail -SkippedBecause NoAzureAccess - return - } - #Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning + Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning return } @@ -66,24 +72,29 @@ function Test-Assessment-25539 { # Query Azure Firewall Policies try { $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01" - #Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + + $policyResponse = Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop + + if ($policyResponse.StatusCode -eq 403) { + Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } - $policyResponseContent = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content + if ($policyResponse.StatusCode -ge 400) { + throw "Firewall policies request failed with status code $($policyResponse.StatusCode)" + } + $policyResponseContent = $policyResponse.Content if (-not $policyResponseContent) { - #Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning + Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning continue } - $policyResponse = $policyResponseContent | ConvertFrom-Json - $policies = $policyResponse.value + $policies = ($policyResponseContent | ConvertFrom-Json).value } catch { - if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { - #Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning - continue - } - #Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning continue } @@ -94,18 +105,28 @@ function Test-Assessment-25539 { foreach ($policyResource in $policies) { try { $detailUri = "$resourceManagerUrl$($policyResource.id)?api-version=2025-03-01" - $detailResponseContent = (Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop).Content + $detailResponse = Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop + + if ($detailResponse.StatusCode -eq 403) { + Write-PSFMessage "Access denied to firewall policy details in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } + + if ($detailResponse.StatusCode -ge 400) { + throw "Firewall policy details request failed with status code $($detailResponse.StatusCode)" + } + $detailResponseContent = $detailResponse.Content if (-not $detailResponseContent) { - #Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning + Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning continue } - $detailResponse = $detailResponseContent | ConvertFrom-Json - $detailedPolicies += $detailResponse + $detailedPolicy = $detailResponseContent | ConvertFrom-Json + $detailedPolicies += $detailedPolicy } catch { - #Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning } } @@ -114,29 +135,24 @@ function Test-Assessment-25539 { # Skip if policy is missing required properties if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { - #Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose continue } # Skip if SKU tier is not Premium if ($policyResource.Properties.sku.tier -ne 'Premium') { - #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose continue } $idMode = $policyResource.Properties.intrusionDetection.mode - # Skip if intrusion detection mode is not configured - if ([string]::IsNullOrEmpty($idMode)) { - #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose - continue - } - # Map intrusion detection mode to user-friendly display values $detectionModeDisplay = switch ($idMode) { 'Deny' { 'Alert and Deny' } 'Alert' { 'Alert Only' } 'Off' { 'Disabled' } + $null { 'Not Configured' } default { $idMode } } @@ -164,10 +180,10 @@ function Test-Assessment-25539 { $passed = $failedPolicies.Count -eq 0 if ($passed) { - $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for all Azure Firewall policies.`n`n%TestResult%" + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for Azure Firewall policies.`n`n%TestResult%" } else { - $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for all Azure Firewall policies.`n`n%TestResult%" + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for Azure Firewall policies.`n`n%TestResult%" } #endregion Assessment Logic From 59aa9d54310fa182e3f9ff273c9d95192ff3b6b2 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 29 Jan 2026 13:22:21 +0530 Subject: [PATCH 04/37] update metadata --- src/powershell/tests/Test-Assessment.25539.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 15075a4af..9cdc9ffcc 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -18,7 +18,7 @@ function Test-Assessment-25539 { Pillar = 'Network', RiskLevel = 'High', SfiPillar = 'Protect networks', - TenantType = ('Workforce'), + TenantType = ('Workforce','External'), TestId = 25539, Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall', UserImpact = 'Low' @@ -222,7 +222,6 @@ function Test-Assessment-25539 { $params = @{ TestId = '25539' - Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall' Status = $passed Result = $testResultMarkdown } From 47fbbf8b5e22e79088f4b9c5a7cf9f14b6b0f986 Mon Sep 17 00:00:00 2001 From: Komal Date: Fri, 30 Jan 2026 09:17:44 +0530 Subject: [PATCH 05/37] update assessment logic --- src/powershell/tests/Test-Assessment.25539.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 9cdc9ffcc..1933e47ee 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -171,10 +171,6 @@ function Test-Assessment-25539 { #endregion Data Collection #region Assessment Logic - if (-not $results) { - Add-ZtTestResultDetail -SkippedBecause NotSupported - return - } $failedPolicies = @($results | Where-Object { -not $_.Passed }) $passed = $failedPolicies.Count -eq 0 From 71b97919917701ecf66b7eb8d604dd8a8f25e516 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 10:32:17 +0530 Subject: [PATCH 06/37] add skip logic --- src/powershell/tests/Test-Assessment.25539.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 1933e47ee..ad9794c75 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -145,6 +145,12 @@ function Test-Assessment-25539 { continue } + # Skip if intrusionDetection is not configured + if (-not $policyResource.Properties.intrusionDetection) { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose + continue + } + $idMode = $policyResource.Properties.intrusionDetection.mode # Map intrusion detection mode to user-friendly display values @@ -186,6 +192,7 @@ function Test-Assessment-25539 { #region Report Generation $reportTitle = "Firewall policies" $tableRows = "" + $mdInfo = "" if ($results.Count -gt 0) { # Create a here-string with format placeholders {0}, {1}, etc. From 7adbf3e469b9d55884acefffb1c224ffd8f78d54 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 12:09:15 +0530 Subject: [PATCH 07/37] add skip logic --- .../tests/Test-Assessment.25539.ps1 | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index ad9794c75..bcdfc330e 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -30,12 +30,32 @@ function Test-Assessment-25539 { #region Data Collection $activity = 'Azure Firewall Intrusion Detection' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Checking Azure connection' + + # Check if connected to Azure + $azContext = Get-AzContext -ErrorAction SilentlyContinue + if (-not $azContext) { + Write-PSFMessage 'Not connected to Azure.' -Level Warning + Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure + return + } + + # Check the supported environment + Write-ZtProgress -Activity $activity -Status 'Checking Azure environment' + 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 + } + Write-ZtProgress ` -Activity $activity ` -Status 'Enumerating Firewall Policies' # Query subscriptions using REST API - $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') + $resourceManagerUrl = $azContext.Environment.ResourceManagerUrl.TrimEnd('/') $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2025-03-01" try { From c899ac403e9c6dd02fd1404b354a1960e0d5f930 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 12:21:53 +0530 Subject: [PATCH 08/37] skip if policy missing --- src/powershell/tests/Test-Assessment.25539.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index bcdfc330e..bd3b06b06 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -198,6 +198,13 @@ function Test-Assessment-25539 { #region Assessment Logic + # If no Premium firewall policies found, skip the test + if ($results.Count -eq 0) { + Write-PSFMessage 'No Azure Firewall Premium policies found to evaluate.' -Tag Firewall -Level Verbose + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + $failedPolicies = @($results | Where-Object { -not $_.Passed }) $passed = $failedPolicies.Count -eq 0 From 0dec2337aa8b13c48294dce94cd67a0f839d4dda Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 12:26:17 +0530 Subject: [PATCH 09/37] add skip reason --- src/powershell/tests/Test-Assessment.25539.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index bd3b06b06..58967254f 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -73,7 +73,7 @@ function Test-Assessment-25539 { $subscriptionsContent = $subscriptionsResponse.Content if (-not $subscriptionsContent) { - Add-ZtTestResultDetail -SkippedBecause NoResults + Add-ZtTestResultDetail -SkippedBecause NotSupported return } @@ -201,7 +201,7 @@ function Test-Assessment-25539 { # If no Premium firewall policies found, skip the test if ($results.Count -eq 0) { Write-PSFMessage 'No Azure Firewall Premium policies found to evaluate.' -Tag Firewall -Level Verbose - Add-ZtTestResultDetail -SkippedBecause NoResults + Add-ZtTestResultDetail -SkippedBecause NotSupported return } From 083f147a3428fd6ea3d54b4e473b1256ee536435 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 13:30:11 +0530 Subject: [PATCH 10/37] fix output and fail condition --- .../tests/Test-Assessment.25539.ps1 | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 58967254f..c2e90f73a 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -165,21 +165,17 @@ function Test-Assessment-25539 { continue } - # Skip if intrusionDetection is not configured - if (-not $policyResource.Properties.intrusionDetection) { - Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose - continue + # Get intrusion detection mode - if not configured, it's disabled by default (FAIL) + $idMode = if ($policyResource.Properties.intrusionDetection) { + $policyResource.Properties.intrusionDetection.mode + } else { + 'Off' } - - $idMode = $policyResource.Properties.intrusionDetection.mode - # Map intrusion detection mode to user-friendly display values $detectionModeDisplay = switch ($idMode) { 'Deny' { 'Alert and Deny' } 'Alert' { 'Alert Only' } 'Off' { 'Disabled' } - $null { 'Not Configured' } - default { $idMode } } $subContext = Get-AzContext @@ -227,8 +223,8 @@ function Test-Assessment-25539 { ## {0} -| Policy name | Subscription name | IDPS Inspection mode | Result | -| :--- | :--- | :--- | :--- | +| Policy name | Subscription name | Result | +| :--- | :--- | :--- | {1} '@ @@ -239,7 +235,8 @@ function Test-Assessment-25539 { $policyMd = "[$(Get-SafeMarkdown -Text $item.PolicyName)]($policyLink)" $subMd = "[$(Get-SafeMarkdown -Text $item.SubscriptionName)]($subLink)" $icon = if ($item.Passed) { '✅' } else { '❌' } - $tableRows += "| $policyMd | $subMd | $($item.IntrusionDetectionMode) | $icon |`n" + $resultText = "$icon $($item.IntrusionDetectionMode)" + $tableRows += "| $policyMd | $subMd | $resultText |`n" } # Format the template by replacing placeholders with values From 972e61e483000c6afb0144f45b94950dd10812b2 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 13:30:51 +0530 Subject: [PATCH 11/37] add new line --- src/powershell/tests/Test-Assessment.25539.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md index 2cdeeb8a1..67c42cb1b 100644 --- a/src/powershell/tests/Test-Assessment.25539.md +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -5,7 +5,8 @@ This check verifies that the Intrusion Detection and Prevention System (IDPS) is If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. **Remediation action** -Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. + +- Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. - [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) From ef5d286aadda14161ee8b89a5b7304593f1a4401 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 14:27:48 +0530 Subject: [PATCH 12/37] added skip before return --- src/powershell/tests/Test-Assessment.25539.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index c2e90f73a..6eb908193 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -81,6 +81,7 @@ function Test-Assessment-25539 { } catch { Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } From d14f7060ab9592c46b465b55a8b25b6722ea7411 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 15:25:53 +0530 Subject: [PATCH 13/37] bug fix for empty subscription --- src/powershell/tests/Test-Assessment.25539.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 6eb908193..ffafbe2a4 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -71,7 +71,7 @@ function Test-Assessment-25539 { throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" } - $subscriptionsContent = $subscriptionsResponse.Content + $subscriptionsContent = $subscriptionsResponse.Content.Value if (-not $subscriptionsContent) { Add-ZtTestResultDetail -SkippedBecause NotSupported return From 10082fa9827bfa828841001a687f35c0cb0fe5dd Mon Sep 17 00:00:00 2001 From: komalp2025 Date: Tue, 3 Feb 2026 15:44:47 +0530 Subject: [PATCH 14/37] Update src/powershell/tests/Test-Assessment.25539.md fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25539.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md index 67c42cb1b..0bba2c3eb 100644 --- a/src/powershell/tests/Test-Assessment.25539.md +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -6,7 +6,7 @@ If this check does not pass, it means that the Intrusion Detection and Preventio **Remediation action** -- Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. +- Please check the IDPS section of this article for guidance on how to enable Intrusion Detection and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. - [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) From d43a0f5532c485593dcebcb7db12badf5652dc60 Mon Sep 17 00:00:00 2001 From: komalp2025 Date: Tue, 3 Feb 2026 15:55:13 +0530 Subject: [PATCH 15/37] Update src/powershell/tests/Test-Assessment.25539.ps1 removing step 3 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25539.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index ffafbe2a4..587400608 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -121,7 +121,7 @@ function Test-Assessment-25539 { if (-not $policies) { continue } - # Step 3: Get individual firewall policy details + # Get individual firewall policy details $detailedPolicies = @() foreach ($policyResource in $policies) { try { From c115e98ae64a7b943d007647d3f6e0f19e2935f8 Mon Sep 17 00:00:00 2001 From: Komal Date: Wed, 4 Feb 2026 11:12:02 +0530 Subject: [PATCH 16/37] improve error handling --- .../tests/Test-Assessment.25539.ps1 | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index ffafbe2a4..4f676f625 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -71,12 +71,7 @@ function Test-Assessment-25539 { throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" } - $subscriptionsContent = $subscriptionsResponse.Content.Value - if (-not $subscriptionsContent) { - Add-ZtTestResultDetail -SkippedBecause NotSupported - return - } - + $subscriptionsContent = $subscriptionsResponse.Content $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value } catch { @@ -88,7 +83,15 @@ function Test-Assessment-25539 { $results = @() foreach ($sub in $subscriptions) { - Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction SilentlyContinue | Out-Null + + # Switch subscription context + try { + Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction Stop | Out-Null + } + catch { + Write-PSFMessage "Unable to switch to subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + continue + } # Query Azure Firewall Policies try { @@ -103,7 +106,8 @@ function Test-Assessment-25539 { } if ($policyResponse.StatusCode -ge 400) { - throw "Firewall policies request failed with status code $($policyResponse.StatusCode)" + Write-PSFMessage "Firewall policies request failed with status code $($policyResponse.StatusCode)" -Tag Firewall -Level Warning + continue } $policyResponseContent = $policyResponse.Content @@ -134,7 +138,8 @@ function Test-Assessment-25539 { } if ($detailResponse.StatusCode -ge 400) { - throw "Firewall policy details request failed with status code $($detailResponse.StatusCode)" + Write-PSFMessage "Firewall policy details request failed with status code $($detailResponse.StatusCode)" -Tag Firewall -Level Warning + continue } $detailResponseContent = $detailResponse.Content @@ -155,20 +160,20 @@ function Test-Assessment-25539 { foreach ($policyResource in $detailedPolicies) { # Skip if policy is missing required properties - if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { + if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.properties) { Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose continue } # Skip if SKU tier is not Premium - if ($policyResource.Properties.sku.tier -ne 'Premium') { + if ($policyResource.properties.sku.tier -ne 'Premium') { Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose continue } # Get intrusion detection mode - if not configured, it's disabled by default (FAIL) - $idMode = if ($policyResource.Properties.intrusionDetection) { - $policyResource.Properties.intrusionDetection.mode + $idMode = if ($policyResource.properties.intrusionDetection) { + $policyResource.properties.intrusionDetection.mode } else { 'Off' } From a1b12aeb00ea2c8e865192c99ad867683ee13b98 Mon Sep 17 00:00:00 2001 From: ashwinikarke <109781888+ashwinikarke@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:09:05 +0530 Subject: [PATCH 17/37] Added helper function for 25377 (#849) * added helper function * updated query * resolved Copilot comments * updated the code --- .../Get-ApplicationNameFromId.ps1 | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 diff --git a/src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 b/src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 new file mode 100644 index 000000000..9d0964dc8 --- /dev/null +++ b/src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 @@ -0,0 +1,82 @@ +function Get-ApplicationNameFromId { + <# + .SYNOPSIS + Resolves application GUIDs to display names from the database. + + .DESCRIPTION + Takes an array of targets (GUIDs or strings) and resolves GUIDs to application display names + by querying the database for ServicePrincipal and Application data. + + .PARAMETER TargetsArray + Array of target values (GUIDs or strings like 'AllApplications') + + .PARAMETER Database + Database connection to query + + .EXAMPLE + $resolved = Get-ApplicationNameFromId -TargetsArray $targets -Database $db + Returns an array of resolved display names + + .OUTPUTS + Array of strings (resolved names or original values) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string[]]$TargetsArray, + + [Parameter(Mandatory = $true)] + $Database + ) + + $displayArray = @() + $targetMap = @{} + # Use HashSet for deduplication of GUIDs to query + $guidsToQuery = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + + # 1. Classification & Deduplication + foreach ($target in $TargetsArray) { + $targetMap[$target] = $target # Default fallback + + $guidRef = [System.Guid]::Empty + if ([System.Guid]::TryParse($target, [ref]$guidRef)) { + [void]$guidsToQuery.Add($target) + } + } + + # 2. Query + if ($guidsToQuery.Count -gt 0) { + try { + # Build IN clause for all GUIDs + $guidInClause = ($guidsToQuery | ForEach-Object { "'$($_.Replace("'", "''"))'" }) -join ',' + + # Single query to resolve all GUIDs at once + $sqlApp = @" +SELECT id, appId, displayName FROM ServicePrincipal WHERE id IN ($guidInClause) OR appId IN ($guidInClause) +UNION +SELECT id, appId, displayName FROM Application WHERE id IN ($guidInClause) OR appId IN ($guidInClause) +"@ + $resolvedApps = Invoke-DatabaseQuery -Database $Database -Sql $sqlApp + + # 3. Build Lookup Hash + foreach ($app in $resolvedApps) { + if (-not [string]::IsNullOrEmpty($app.displayName)) { + # Handle DB returning Guid objects by forcing string conversion for keys + if ($app.id) { $targetMap["$($app.id)"] = $app.displayName } + if ($app.appId) { $targetMap["$($app.appId)"] = $app.displayName } + } + } + } + catch { + Write-PSFMessage -Level Warning -Message "Failed to resolve application GUIDs from database: $_" + } + } + + # 4. Reconstruct Output + foreach ($target in $TargetsArray) { + $displayArray += $targetMap[$target] + } + + # Comma operator prevents PowerShell from unrolling single-element arrays + return ,$displayArray +} From 24d65e52bdd0f17db7afa73e1ab6d4f4bb36113c Mon Sep 17 00:00:00 2001 From: komalp2025 Date: Wed, 4 Feb 2026 14:48:02 +0530 Subject: [PATCH 18/37] Data 35039 Copilot Communication Compliance Monitoring Configured (#776) * draft - 35039 * add 35039 * remove redundant title * remove array wrapper * Fix links in Test-Assessment.35039.md Updated links in Test-Assessment.35039.md for accuracy. --- src/powershell/tests/Test-Assessment.35039.md | 39 +++ .../tests/Test-Assessment.35039.ps1 | 253 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.35039.md create mode 100644 src/powershell/tests/Test-Assessment.35039.ps1 diff --git a/src/powershell/tests/Test-Assessment.35039.md b/src/powershell/tests/Test-Assessment.35039.md new file mode 100644 index 000000000..ab1d058f8 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35039.md @@ -0,0 +1,39 @@ +Communication Compliance policies with Copilot content detection enable organizations to monitor and investigate how users interact with Microsoft Copilot in Teams, Outlook, and Microsoft 365 apps. Without Communication Compliance policies configured to capture Copilot interactions, organizations cannot detect when sensitive data is being exposed to AI services, how users are leveraging Copilot with confidential information, or detect potential policy violations involving AI-assisted data processing. + +Copilot interaction capture through Communication Compliance enables organizations to implement governance and oversight of AI usage while maintaining user communication privacy controls. Users may unknowingly expose sensitive data (customer records, financial information, source code, trade secrets) to Copilot, creating a data spillage risk that becomes invisible without activity monitoring. Organizations must enable Communication Compliance policies targeting Copilot interactions to maintain visibility into how AI features are being used with sensitive data and ensure compliance with data governance policies. + +**Remediation action** + +To create and enable Communication Compliance policies for Copilot interaction capture: + +1. Sign in as a Global Administrator or Compliance Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) +2. Navigate to Communication Compliance > Policies +3. Select "+ Create policy" to start the policy creation workflow +4. Choose the "Monitor for sensitive content" template or create a custom policy +5. Name the policy (e.g., "Copilot Data Protection") +6. Configure the scope (all users or specific groups) +7. On the Conditions page, add conditions to detect: + - Sensitive information types (credit cards, SSN, financial data) + - Keywords related to confidential data + - Custom patterns for your organization's sensitive data +8. On the Review settings page, configure: + - Reviewers (compliance team members) + - Alert volume preference + - Review mailbox for alerts +9. Enable the policy +10. Verify rule creation via PowerShell using Query 1 and 2 + +Via PowerShell (creation requires portal, but verification via cmdlets): + +```powershell +Connect-ExchangeOnline +Get-SupervisoryReviewRule -IncludeRuleXml | Select-Object Name, Policy +Get-SupervisoryReviewPolicyV2 | Select-Object Name, Enabled, ReviewMailbox +``` + +For more information: +- [Communication Compliance overview](https://learn.microsoft.com/en-us/purview/communication-compliance) +- [Create Communication Compliance policies](https://learn.microsoft.com/en-us/purview/communication-compliance-policies) +- [SupervisoryReview cmdlet reference](https://learn.microsoft.com/en-us/powershell/module/exchange/get-supervisoryreviewpolicyv2) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35039.ps1 b/src/powershell/tests/Test-Assessment.35039.ps1 new file mode 100644 index 000000000..1b38e74ea --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35039.ps1 @@ -0,0 +1,253 @@ +<# +.SYNOPSIS + Validates that Communication Compliance rules are configured to detect and monitor Copilot content. + +.DESCRIPTION + This test verifies that Communication Compliance rules targeting Copilot interactions are properly + configured and enabled. It checks that supervisory review policies with Copilot-targeting rules + are active and have configured review mailboxes for processing alerts. + +.NOTES + Test ID: 35039 + Category: Data Security Posture Management + Pillar: Data + Required Module: ExchangeOnlineManagement + Required Connection: Security & Compliance PowerShell +#> + +function Test-Assessment-35039 { + [ZtTest( + Category = 'Data Security Posture Management', + ImplementationCost = 'Medium', + MinimumLicense = ('Microsoft 365 E5'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce'), + TestId = 35039, + Title = 'Copilot Communication Compliance Monitoring Configured', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Communication Compliance Rules for Copilot Content' + Write-ZtProgress -Activity $activity -Status 'Getting supervisory review rules' + + # Q1: Find Communication Compliance rules targeting Copilot content + $copilotRules = @() + $errorMsg = $null + + try { + $allRules = Get-SupervisoryReviewRule -IncludeRuleXml -ErrorAction Stop + $allReviewPolicy = Get-SupervisoryReviewPolicyV2 + + foreach ($rule in $allRules) { + if (-not [string]::IsNullOrWhiteSpace($rule.RuleXml)) { + try { + # Wrap RuleXml in a root element to handle multiple rule elements + $wrappedXml = "$($rule.RuleXml)" + $ruleXml = [xml]$wrappedXml + $hasCopilotConfig = $false + + # Check for Copilot in Workloads array within JSON value elements + if ($ruleXml.root) { + $valueElements = $ruleXml.root.GetElementsByTagName('value') + foreach ($valueElement in $valueElements) { + if (-not [string]::IsNullOrWhiteSpace($valueElement.'#text')) { + try { + $jsonData = $valueElement.'#text' | ConvertFrom-Json + if ($jsonData.Workloads -and $jsonData.Workloads -contains 'Copilot') { + $hasCopilotConfig = $true + break + } + } + catch { + # Skip if JSON parsing fails + } + } + } + } + + if ($hasCopilotConfig) { + # Lookup policy name from $allReviewPolicy using Policy ID + $policyId = $rule.Policy + $policyName = ($allReviewPolicy | Where-Object { $_.Guid -eq $policyId }).Name + + $copilotRules += [PSCustomObject]@{ + RuleName = $rule.Name + PolicyId = $policyId + PolicyName = if ($policyName) { $policyName } else { 'Unknown' } + } + } + } + catch { + Write-PSFMessage "Error parsing RuleXml for rule '$($rule.Name)': $_" -Level Warning + } + } + + } + } + catch { + $errorMsg = $_ + Write-PSFMessage "Failed to retrieve supervisory review rules: $_" -Tag Test -Level Warning + } + + # Q2: Resolve Copilot-targeting policies and verify enabled status + $enabledCopilotPolicies = @() + if ($copilotRules -and -not $errorMsg) { + #Write-ZtProgress -Activity $activity -Status 'Verifying policy enabled status' + + try { + $copilotPolicyIdentities = @($copilotRules | Select-Object -ExpandProperty PolicyId -Unique) + $policies = foreach ($id in $copilotPolicyIdentities) { + $allReviewPolicy | Where-Object { $_.Guid -eq $id } + } + $enabledCopilotPolicies = @($policies | Where-Object { $_ -and $_.Enabled -eq $true }) + } + catch { + Write-PSFMessage "Failed to retrieve supervisory review policies: $_" -Tag Test -Level Warning + } + } + + # Q3: Verify Copilot capture is active by checking audit logs (optional) + $policyHits = $null + if ($enabledCopilotPolicies) { + Write-ZtProgress -Activity $activity -Status 'Checking audit logs' + + try { + $startDate = (Get-Date).AddDays(-30) + $endDate = Get-Date + $hits = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -Operations SupervisionRuleMatch -ErrorAction Stop + + if ($hits) { + $policyNamePattern = ($enabledCopilotPolicies.Name | ForEach-Object { [regex]::Escape($_) }) -join '|' + $policyHits = @($hits | Where-Object { $_.AuditData -match $policyNamePattern -and ($_.AuditData -match 'Copilot') }) + } + } + catch { + Write-PSFMessage "Failed to check audit logs: $_" -Tag Test -Level Warning + } + } + #endregion Data Collection + + #region Assessment Logic + $passed = $false + + # Evaluation Logic: + # 1. If Query 1 returns at least 1 rule with Copilot in RuleXml, proceed to Query 2 + if ($copilotRules.Count -gt 0) { + # 2. If Query 2 returns at least 1 enabled policy with ReviewMailbox configured, then Pass + $hasValidPolicies = @($enabledCopilotPolicies | Where-Object { $_.ReviewMailbox }).Count -gt 0 + $passed = $hasValidPolicies + } + # 3. If Query 1 returns no rules or Query 2 returns no enabled policies, then Fail + else { + $passed = $false + } + # Query 3 (audit logs) is optional and used only for evidence display + #endregion Assessment Logic + + #region Report Generation + $mdInfo = '' + + if ($passed) { + $statusIcon = '✅ Pass' + $statusMessage = 'Communication Compliance rules targeting Copilot content are properly configured and enabled.' + } + else { + $statusIcon = '❌ Fail' + $statusMessage = 'Communication Compliance rules targeting Copilot content are not properly configured or enabled.' + } + + # Copilot-Targeting Rules section + if ($copilotRules -and $copilotRules.Count -gt 0) { + $rulesTableRows = '' + foreach ($rule in $copilotRules | Sort-Object RuleName) { + $rulesTableRows += "| $($rule.RuleName) | $($rule.PolicyName) |`n" + } + + $rulesTemplate = @' + +### Copilot-Targeting Rules + +| Rule Name | Associated Policy | +| :------ | :---- | +{0} +'@ + $mdInfo += $rulesTemplate -f $rulesTableRows + } + else { + $mdInfo += "`n### Copilot-Targeting Rules`n`nNo Copilot-targeting rules found.`n" + } + + # Enabled Policies section + if ($enabledCopilotPolicies -and $enabledCopilotPolicies.Count -gt 0) { + $policiesTableRows = '' + foreach ($policy in $enabledCopilotPolicies | Sort-Object Name) { + $reviewMailbox = if ($policy.ReviewMailbox) { $policy.ReviewMailbox } else { 'Not configured' } + $enabledStatus = if ($policy.Enabled -eq $true) { 'True' } else { 'False' } + $policiesTableRows += "| $($policy.Name) | $enabledStatus | $reviewMailbox |`n" + } + + $policiesTemplate = @' + +### Enabled Policies + +| Policy Name | Enabled | Review Mailbox | +| :------ | :---- | :---- | +{0} +'@ + $mdInfo += $policiesTemplate -f $policiesTableRows + } + else { + $mdInfo += "`n### Enabled Policies`n`nNo enabled policies with Copilot rules found.`n" + } + + # Activity Evidence section + $evidenceText = if ($policyHits -and $policyHits.Count -gt 0) { + "Recent Copilot Matches (30 days): $($policyHits.Count)" + } + elseif ($enabledCopilotPolicies -and $enabledCopilotPolicies.Count -gt 0) { + "Recent Copilot Matches (30 days): 0" + } + else { + "Recent Copilot Matches (30 days): No policies configured for audit review." + } + + $mdInfo += "`n### Activity Evidence`n`n$evidenceText`n" + + # Summary + $summaryTemplate = @' + +**Summary:** + + Status: {0} + + Total Copilot Rules Found: {1} + + Enabled Policies with Copilot Rules: {2} + +**Portal Access:** + + [Microsoft Purview Communication Compliance > Policies](https://purview.microsoft.com/communicationcompliance/policies) + +'@ + + $mdInfo += $summaryTemplate -f $statusIcon, $copilotRules.Count, $enabledCopilotPolicies.Count + + $testResultMarkdown = "$statusMessage`n$mdInfo" + + #endregion Report Generation + + $params = @{ + TestId = '35039' + Status = $passed + Result = $testResultMarkdown + } + + Add-ZtTestResultDetail @params +} From 3e1063505642707986b8e8ba2b4a281da0c2d0c9 Mon Sep 17 00:00:00 2001 From: Naga Praneeth Chukka Date: Thu, 5 Feb 2026 09:44:47 +0530 Subject: [PATCH 19/37] Added NotApplicable reason to Get-ZtSkippedReason function (#852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added NotApplicable reason * Update message for 'NotApplicable' case in Get-ZtSkippedReason --------- Co-authored-by: Aleksandar Nikolić --- src/powershell/private/core/Add-ZtTestResultDetail.ps1 | 2 +- src/powershell/private/core/Get-ZtSkippedReason.ps1 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/powershell/private/core/Add-ZtTestResultDetail.ps1 b/src/powershell/private/core/Add-ZtTestResultDetail.ps1 index 13f81ddaa..be3d7191f 100644 --- a/src/powershell/private/core/Add-ZtTestResultDetail.ps1 +++ b/src/powershell/private/core/Add-ZtTestResultDetail.ps1 @@ -67,7 +67,7 @@ function Add-ZtTestResultDetail { [ValidateSet('NotConnectedAzure', 'NotConnectedExchange', 'NotDotGovDomain', 'NotLicensedEntraIDP1', 'NotConnectedSecurityCompliance', 'NotLicensedEntraIDP2', 'NotLicensedEntraIDGovernance', 'NotLicensedEntraWorkloadID', 'NotSupported', 'UnderConstruction', - 'NotLicensedIntune', 'NoAzureAccess' + 'NotLicensedIntune', 'NoAzureAccess', 'NotApplicable' )] [string] $SkippedBecause, diff --git a/src/powershell/private/core/Get-ZtSkippedReason.ps1 b/src/powershell/private/core/Get-ZtSkippedReason.ps1 index a8b0571ce..3a896a46d 100644 --- a/src/powershell/private/core/Get-ZtSkippedReason.ps1 +++ b/src/powershell/private/core/Get-ZtSkippedReason.ps1 @@ -21,6 +21,7 @@ function Get-ZtSkippedReason { "NotLicensedIntune" { "This test is for tenants that are licensed for Microsoft Intune. See [Microsoft Intune licensing](https://learn.microsoft.com/intune/intune-service/fundamentals/licenses)"; break} "NotSupported" { "This test relies on capabilities not currently available (e.g., cmdlets that are not available on all platforms, Resolve-DnsName)"; break} "NoAzureAccess" { "The signed in user does not have access to the Azure subscription to perform this test."; break} + "NotApplicable" { "This test is not applicable to the current environment."; break} default { $SkippedBecause; break} } } From 7fce5081f2d368ba6061748d3ad7e435a5d529a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:34:06 +1100 Subject: [PATCH 20/37] Bump @modelcontextprotocol/sdk from 1.24.0 to 1.26.0 (#859) Bumps [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) from 1.24.0 to 1.26.0. - [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases) - [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/1.24.0...v1.26.0) --- updated-dependencies: - dependency-name: "@modelcontextprotocol/sdk" dependency-version: 1.26.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 218 ++++++++++++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5837447ee..b5c56cee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -624,6 +623,19 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", @@ -786,12 +798,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.0.tgz", - "integrity": "sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -799,13 +812,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -847,7 +862,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -1115,9 +1129,9 @@ } }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { @@ -1127,7 +1141,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -1177,7 +1191,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -1432,16 +1445,17 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -1860,7 +1874,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1900,11 +1913,14 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "dev": true, "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -2038,9 +2054,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -2052,7 +2068,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/formdata-polyfill": { @@ -2305,31 +2325,35 @@ "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "dev": true, "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": ">=16.9.0" } }, - "node_modules/http-errors/node_modules/statuses": { + "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/https-proxy-agent": { @@ -2357,9 +2381,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -2407,6 +2431,16 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2633,6 +2667,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2815,16 +2856,20 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { @@ -3265,7 +3310,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3424,16 +3468,16 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" @@ -3601,27 +3645,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3640,32 +3663,36 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -3676,6 +3703,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -4402,15 +4433,14 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", - "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "dev": true, "license": "ISC", "peerDependencies": { From ef13d399549e770b5c86a3ce06a7cc419b2bfe62 Mon Sep 17 00:00:00 2001 From: Komal Date: Fri, 23 Jan 2026 18:25:04 +0530 Subject: [PATCH 21/37] add 25539 --- src/powershell/tests/Test-Assessment.25539.md | 12 ++ .../tests/Test-Assessment.25539.ps1 | 182 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.25539.md create mode 100644 src/powershell/tests/Test-Assessment.25539.ps1 diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md new file mode 100644 index 000000000..a880f21b0 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -0,0 +1,12 @@ +Azure Firewall Premium offers signature-based IDPS to quickly detect attacks by identifying specific patterns, such as byte sequences in network traffic or known malicious instruction sequences used by malware. These IDPS signatures apply to both application and network-level traffic (Layers 3-7). They are fully managed and continuously updated. IDPS can be applied to inbound, spoke-to-spoke (East-West), and outbound traffic, including traffic to/from an on-premises network. + +This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. + +If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. + +**Remediation action** +Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. + +- [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 new file mode 100644 index 000000000..302f04b24 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -0,0 +1,182 @@ +<# +.SYNOPSIS + Validates Intrusion Detection is Enabled in Deny Mode on Azure Firewall. +.DESCRIPTION + This test validates that Azure Firewall Policies have Intrusion Detection enabled in Deny mode. + Checks all firewall policies in the subscription and reports their intrusion detection status. +.NOTES + Test ID: 25539 + Category: Azure Network Security + Required API: Azure Firewall Policies +#> + +function Test-Assessment-25539 { + [ZtTest( + Category = 'Azure Network Security', + ImplementationCost = 'Low', + MinimumLicense = ('Azure_Firewall_Premium'), + Pillar = 'Network', + RiskLevel = 'High', + SfiPillar = 'Protect networks', + TenantType = ('Workforce'), + TestId = 25539, + Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + #region Data Collection + $activity = 'Azure Firewall Intrusion Detection' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Enumerating Firewall Policies' + + # Query subscriptions using REST API + $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') + $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2020-01-01" + + try { + $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop + } + catch { + if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + } + + $subscriptions = ($subscriptionsResponse.Content | ConvertFrom-Json).value + + if (-not $subscriptions) { + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + + $results = @() + + foreach ($sub in $subscriptions) { + + Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction SilentlyContinue | Out-Null + + # Query Azure Firewall Policies + try { + $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2023-04-01" + Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + + $policyResponse = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content | ConvertFrom-Json + $policies = $policyResponse.value + + } + catch { + Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + continue + } + + if (-not $policies) { continue } + + # Check intrusion detection mode for each firewall policy + foreach ($policyResource in $policies) { + + # Skip if policy is missing required properties + if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { + Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + continue + } + + # Skip if SKU tier is not Premium + if ($policyResource.Properties.sku.tier -ne 'Premium') { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + continue + } + + $idMode = $policyResource.Properties.intrusionDetection.mode + + # Skip if intrusion detection mode is not configured + if ([string]::IsNullOrEmpty($idMode)) { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose + continue + } + + # Map intrusion detection mode to user-friendly display values + $detectionModeDisplay = switch ($idMode) { + 'Deny' { 'Alert and Deny' } + 'Alert' { 'Alert Only' } + 'Off' { 'Disabled' } + default { $idMode } + } + + $subContext = Get-AzContext + + $results += [PSCustomObject]@{ + PolicyName = $policyResource.Name + SubscriptionName = $subContext.Subscription.Name + SubscriptionId = $subContext.Subscription.Id + IntrusionDetectionMode = $detectionModeDisplay + PolicyID = $policyResource.Id + Passed = $idMode -eq 'Deny' + } + } + } + #endregion Data Collection + + #region Assessment Logic + if (-not $results) { + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + + $passed = ($results | Where-Object { -not $_.Passed }).Count -eq 0 + + if ($passed) { + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for all Azure Firewall policies.`n`n%TestResult%" + } + else { + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for all Azure Firewall policies.`n`n%TestResult%" + } + #endregion Assessment Logic + + #region Report Generation + $reportTitle = "Firewall policies" + $tableRows = "" + + if ($results.Count -gt 0) { + # Create a here-string with format placeholders {0}, {1}, etc. + $formatTemplate = @' + +## {0} + +| Policy name | Subscription name | IDPS Inspection mode | Result | +| :--- | :--- | :--- | :--- | +{1} + +'@ + + foreach ($item in $results | Sort-Object PolicyName) { + $policyLink = "https://portal.azure.com/#resource$($item.PolicyID)" + $subLink = "https://portal.azure.com/#resource/subscriptions/$($item.SubscriptionId)" + $policyMd = "[$(Get-SafeMarkdown -Text $item.PolicyName)]($policyLink)" + $subMd = "[$(Get-SafeMarkdown -Text $item.SubscriptionName)]($subLink)" + $icon = if ($item.Passed) { '✅' } else { '❌' } + $tableRows += "| $policyMd | $subMd | $($item.IntrusionDetectionMode) | $icon |`n" + } + + # Format the template by replacing placeholders with values + $mdInfo = $formatTemplate -f $reportTitle, $tableRows + } + + # Replace the placeholder with the detailed information + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '25539' + Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall' + Status = $passed + Result = $testResultMarkdown + } + + Add-ZtTestResultDetail @params +} From 72a2ddfaf009c32ca88edf7b9ac55d44507962f1 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 29 Jan 2026 12:45:18 +0530 Subject: [PATCH 22/37] Add test 25539 --- src/powershell/tests/Test-Assessment.25539.md | 4 +- .../tests/Test-Assessment.25539.ps1 | 81 +++++++++++++------ 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md index a880f21b0..2cdeeb8a1 100644 --- a/src/powershell/tests/Test-Assessment.25539.md +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -1,11 +1,11 @@ Azure Firewall Premium offers signature-based IDPS to quickly detect attacks by identifying specific patterns, such as byte sequences in network traffic or known malicious instruction sequences used by malware. These IDPS signatures apply to both application and network-level traffic (Layers 3-7). They are fully managed and continuously updated. IDPS can be applied to inbound, spoke-to-spoke (East-West), and outbound traffic, including traffic to/from an on-premises network. -This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. +This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. **Remediation action** -Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. +Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. - [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 302f04b24..4e8d5351c 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -26,69 +26,101 @@ function Test-Assessment-25539 { [CmdletBinding()] param() - Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + #Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose #region Data Collection $activity = 'Azure Firewall Intrusion Detection' - Write-ZtProgress ` - -Activity $activity ` - -Status 'Enumerating Firewall Policies' + #Write-ZtProgress ` + #-Activity $activity ` + #-Status 'Enumerating Firewall Policies' # Query subscriptions using REST API $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') - $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2020-01-01" + $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2025-03-01" try { $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop + $subscriptionsContent = $subscriptionsResponse.Content + + if (-not $subscriptionsContent) { + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + + $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value } catch { if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { - Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + # Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } - } - - $subscriptions = ($subscriptionsResponse.Content | ConvertFrom-Json).value - - if (-not $subscriptions) { - Add-ZtTestResultDetail -SkippedBecause NoResults + #Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning return } $results = @() foreach ($sub in $subscriptions) { - Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction SilentlyContinue | Out-Null # Query Azure Firewall Policies try { - $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2023-04-01" - Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01" + #Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" - $policyResponse = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content | ConvertFrom-Json - $policies = $policyResponse.value + $policyResponseContent = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content + + if (-not $policyResponseContent) { + #Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning + continue + } + $policyResponse = $policyResponseContent | ConvertFrom-Json + $policies = $policyResponse.value } catch { - Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { + #Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } + #Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning continue } if (-not $policies) { continue } - # Check intrusion detection mode for each firewall policy + # Step 3: Get individual firewall policy details + $detailedPolicies = @() foreach ($policyResource in $policies) { + try { + $detailUri = "$resourceManagerUrl$($policyResource.id)?api-version=2025-03-01" + $detailResponseContent = (Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop).Content + + if (-not $detailResponseContent) { + #Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning + continue + } + + $detailResponse = $detailResponseContent | ConvertFrom-Json + $detailedPolicies += $detailResponse + } + catch { + #Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + } + } + + # Check intrusion detection mode for each firewall policy + foreach ($policyResource in $detailedPolicies) { # Skip if policy is missing required properties if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { - Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + #Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose continue } # Skip if SKU tier is not Premium if ($policyResource.Properties.sku.tier -ne 'Premium') { - Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose continue } @@ -96,7 +128,7 @@ function Test-Assessment-25539 { # Skip if intrusion detection mode is not configured if ([string]::IsNullOrEmpty($idMode)) { - Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose + #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose continue } @@ -124,11 +156,12 @@ function Test-Assessment-25539 { #region Assessment Logic if (-not $results) { - Add-ZtTestResultDetail -SkippedBecause NoResults + Add-ZtTestResultDetail -SkippedBecause NotSupported return } - $passed = ($results | Where-Object { -not $_.Passed }).Count -eq 0 + $failedPolicies = @($results | Where-Object { -not $_.Passed }) + $passed = $failedPolicies.Count -eq 0 if ($passed) { $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for all Azure Firewall policies.`n`n%TestResult%" From 55bef42588352301a910b46fd33454715e98da3b Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 29 Jan 2026 13:19:50 +0530 Subject: [PATCH 23/37] updated user facing message --- .../tests/Test-Assessment.25539.ps1 | 84 +++++++++++-------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 4e8d5351c..15075a4af 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -30,9 +30,9 @@ function Test-Assessment-25539 { #region Data Collection $activity = 'Azure Firewall Intrusion Detection' - #Write-ZtProgress ` - #-Activity $activity ` - #-Status 'Enumerating Firewall Policies' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Enumerating Firewall Policies' # Query subscriptions using REST API $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') @@ -40,8 +40,18 @@ function Test-Assessment-25539 { try { $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop - $subscriptionsContent = $subscriptionsResponse.Content + if ($subscriptionsResponse.StatusCode -eq 403) { + Write-PSFMessage 'The signed in user does not have access to check subscriptions.' -Level Verbose + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + + if ($subscriptionsResponse.StatusCode -ge 400) { + throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" + } + + $subscriptionsContent = $subscriptionsResponse.Content if (-not $subscriptionsContent) { Add-ZtTestResultDetail -SkippedBecause NoResults return @@ -50,11 +60,7 @@ function Test-Assessment-25539 { $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value } catch { - if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { - # Add-ZtTestResultDetail -SkippedBecause NoAzureAccess - return - } - #Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning + Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning return } @@ -66,24 +72,29 @@ function Test-Assessment-25539 { # Query Azure Firewall Policies try { $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01" - #Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + + $policyResponse = Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop + + if ($policyResponse.StatusCode -eq 403) { + Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } - $policyResponseContent = (Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop).Content + if ($policyResponse.StatusCode -ge 400) { + throw "Firewall policies request failed with status code $($policyResponse.StatusCode)" + } + $policyResponseContent = $policyResponse.Content if (-not $policyResponseContent) { - #Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning + Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning continue } - $policyResponse = $policyResponseContent | ConvertFrom-Json - $policies = $policyResponse.value + $policies = ($policyResponseContent | ConvertFrom-Json).value } catch { - if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Forbidden*') { - #Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning - continue - } - #Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning continue } @@ -94,18 +105,28 @@ function Test-Assessment-25539 { foreach ($policyResource in $policies) { try { $detailUri = "$resourceManagerUrl$($policyResource.id)?api-version=2025-03-01" - $detailResponseContent = (Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop).Content + $detailResponse = Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop + + if ($detailResponse.StatusCode -eq 403) { + Write-PSFMessage "Access denied to firewall policy details in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } + + if ($detailResponse.StatusCode -ge 400) { + throw "Firewall policy details request failed with status code $($detailResponse.StatusCode)" + } + $detailResponseContent = $detailResponse.Content if (-not $detailResponseContent) { - #Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning + Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning continue } - $detailResponse = $detailResponseContent | ConvertFrom-Json - $detailedPolicies += $detailResponse + $detailedPolicy = $detailResponseContent | ConvertFrom-Json + $detailedPolicies += $detailedPolicy } catch { - #Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning } } @@ -114,29 +135,24 @@ function Test-Assessment-25539 { # Skip if policy is missing required properties if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { - #Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose continue } # Skip if SKU tier is not Premium if ($policyResource.Properties.sku.tier -ne 'Premium') { - #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose continue } $idMode = $policyResource.Properties.intrusionDetection.mode - # Skip if intrusion detection mode is not configured - if ([string]::IsNullOrEmpty($idMode)) { - #Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose - continue - } - # Map intrusion detection mode to user-friendly display values $detectionModeDisplay = switch ($idMode) { 'Deny' { 'Alert and Deny' } 'Alert' { 'Alert Only' } 'Off' { 'Disabled' } + $null { 'Not Configured' } default { $idMode } } @@ -164,10 +180,10 @@ function Test-Assessment-25539 { $passed = $failedPolicies.Count -eq 0 if ($passed) { - $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for all Azure Firewall policies.`n`n%TestResult%" + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for Azure Firewall policies.`n`n%TestResult%" } else { - $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for all Azure Firewall policies.`n`n%TestResult%" + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for Azure Firewall policies.`n`n%TestResult%" } #endregion Assessment Logic From 14cbbf611f424ed4b63c93ecfabd92af0dba2859 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 29 Jan 2026 13:22:21 +0530 Subject: [PATCH 24/37] update metadata --- src/powershell/tests/Test-Assessment.25539.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 15075a4af..9cdc9ffcc 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -18,7 +18,7 @@ function Test-Assessment-25539 { Pillar = 'Network', RiskLevel = 'High', SfiPillar = 'Protect networks', - TenantType = ('Workforce'), + TenantType = ('Workforce','External'), TestId = 25539, Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall', UserImpact = 'Low' @@ -222,7 +222,6 @@ function Test-Assessment-25539 { $params = @{ TestId = '25539' - Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall' Status = $passed Result = $testResultMarkdown } From 9e04788370d3378b91548b3c7d5fda5adfce3ebd Mon Sep 17 00:00:00 2001 From: Komal Date: Fri, 30 Jan 2026 09:17:44 +0530 Subject: [PATCH 25/37] update assessment logic --- src/powershell/tests/Test-Assessment.25539.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 9cdc9ffcc..1933e47ee 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -171,10 +171,6 @@ function Test-Assessment-25539 { #endregion Data Collection #region Assessment Logic - if (-not $results) { - Add-ZtTestResultDetail -SkippedBecause NotSupported - return - } $failedPolicies = @($results | Where-Object { -not $_.Passed }) $passed = $failedPolicies.Count -eq 0 From cda9e67bcf9f5bf0d438ee6ef052c8461e40451d Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 10:32:17 +0530 Subject: [PATCH 26/37] add skip logic --- src/powershell/tests/Test-Assessment.25539.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 1933e47ee..ad9794c75 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -145,6 +145,12 @@ function Test-Assessment-25539 { continue } + # Skip if intrusionDetection is not configured + if (-not $policyResource.Properties.intrusionDetection) { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose + continue + } + $idMode = $policyResource.Properties.intrusionDetection.mode # Map intrusion detection mode to user-friendly display values @@ -186,6 +192,7 @@ function Test-Assessment-25539 { #region Report Generation $reportTitle = "Firewall policies" $tableRows = "" + $mdInfo = "" if ($results.Count -gt 0) { # Create a here-string with format placeholders {0}, {1}, etc. From b5c57095c10fdcab38ead058b3a6a1b1f9ccabc0 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 12:09:15 +0530 Subject: [PATCH 27/37] add skip logic --- .../tests/Test-Assessment.25539.ps1 | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index ad9794c75..bcdfc330e 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -30,12 +30,32 @@ function Test-Assessment-25539 { #region Data Collection $activity = 'Azure Firewall Intrusion Detection' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Checking Azure connection' + + # Check if connected to Azure + $azContext = Get-AzContext -ErrorAction SilentlyContinue + if (-not $azContext) { + Write-PSFMessage 'Not connected to Azure.' -Level Warning + Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure + return + } + + # Check the supported environment + Write-ZtProgress -Activity $activity -Status 'Checking Azure environment' + 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 + } + Write-ZtProgress ` -Activity $activity ` -Status 'Enumerating Firewall Policies' # Query subscriptions using REST API - $resourceManagerUrl = (Get-AzContext).Environment.ResourceManagerUrl.TrimEnd('/') + $resourceManagerUrl = $azContext.Environment.ResourceManagerUrl.TrimEnd('/') $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2025-03-01" try { From 2e91603a6b2b88f849a8af4afc5154a18a168710 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 12:21:53 +0530 Subject: [PATCH 28/37] skip if policy missing --- src/powershell/tests/Test-Assessment.25539.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index bcdfc330e..bd3b06b06 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -198,6 +198,13 @@ function Test-Assessment-25539 { #region Assessment Logic + # If no Premium firewall policies found, skip the test + if ($results.Count -eq 0) { + Write-PSFMessage 'No Azure Firewall Premium policies found to evaluate.' -Tag Firewall -Level Verbose + Add-ZtTestResultDetail -SkippedBecause NoResults + return + } + $failedPolicies = @($results | Where-Object { -not $_.Passed }) $passed = $failedPolicies.Count -eq 0 From 9a37c4f8131433429509d9081b1b9b81d8ae78e8 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 12:26:17 +0530 Subject: [PATCH 29/37] add skip reason --- src/powershell/tests/Test-Assessment.25539.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index bd3b06b06..58967254f 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -73,7 +73,7 @@ function Test-Assessment-25539 { $subscriptionsContent = $subscriptionsResponse.Content if (-not $subscriptionsContent) { - Add-ZtTestResultDetail -SkippedBecause NoResults + Add-ZtTestResultDetail -SkippedBecause NotSupported return } @@ -201,7 +201,7 @@ function Test-Assessment-25539 { # If no Premium firewall policies found, skip the test if ($results.Count -eq 0) { Write-PSFMessage 'No Azure Firewall Premium policies found to evaluate.' -Tag Firewall -Level Verbose - Add-ZtTestResultDetail -SkippedBecause NoResults + Add-ZtTestResultDetail -SkippedBecause NotSupported return } From b679385b8ad64262e871ef318101a63ac0738e2c Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 13:30:11 +0530 Subject: [PATCH 30/37] fix output and fail condition --- .../tests/Test-Assessment.25539.ps1 | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 58967254f..c2e90f73a 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -165,21 +165,17 @@ function Test-Assessment-25539 { continue } - # Skip if intrusionDetection is not configured - if (-not $policyResource.Properties.intrusionDetection) { - Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have intrusion detection configured. Skipping." -Tag Firewall -Level Verbose - continue + # Get intrusion detection mode - if not configured, it's disabled by default (FAIL) + $idMode = if ($policyResource.Properties.intrusionDetection) { + $policyResource.Properties.intrusionDetection.mode + } else { + 'Off' } - - $idMode = $policyResource.Properties.intrusionDetection.mode - # Map intrusion detection mode to user-friendly display values $detectionModeDisplay = switch ($idMode) { 'Deny' { 'Alert and Deny' } 'Alert' { 'Alert Only' } 'Off' { 'Disabled' } - $null { 'Not Configured' } - default { $idMode } } $subContext = Get-AzContext @@ -227,8 +223,8 @@ function Test-Assessment-25539 { ## {0} -| Policy name | Subscription name | IDPS Inspection mode | Result | -| :--- | :--- | :--- | :--- | +| Policy name | Subscription name | Result | +| :--- | :--- | :--- | {1} '@ @@ -239,7 +235,8 @@ function Test-Assessment-25539 { $policyMd = "[$(Get-SafeMarkdown -Text $item.PolicyName)]($policyLink)" $subMd = "[$(Get-SafeMarkdown -Text $item.SubscriptionName)]($subLink)" $icon = if ($item.Passed) { '✅' } else { '❌' } - $tableRows += "| $policyMd | $subMd | $($item.IntrusionDetectionMode) | $icon |`n" + $resultText = "$icon $($item.IntrusionDetectionMode)" + $tableRows += "| $policyMd | $subMd | $resultText |`n" } # Format the template by replacing placeholders with values From 595a0a03ac8e0b094fc0bbb4c4e9f1d727766bd4 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 13:30:51 +0530 Subject: [PATCH 31/37] add new line --- src/powershell/tests/Test-Assessment.25539.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md index 2cdeeb8a1..67c42cb1b 100644 --- a/src/powershell/tests/Test-Assessment.25539.md +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -5,7 +5,8 @@ This check verifies that the Intrusion Detection and Prevention System (IDPS) is If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. **Remediation action** -Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. + +- Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. - [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) From 1f2d63b71c9a55d72066cad8d70e93be6addb267 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 14:27:48 +0530 Subject: [PATCH 32/37] added skip before return --- src/powershell/tests/Test-Assessment.25539.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index c2e90f73a..6eb908193 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -81,6 +81,7 @@ function Test-Assessment-25539 { } catch { Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } From 3c3f8e7ca69fcc52e802556eb23aa319375ed9c9 Mon Sep 17 00:00:00 2001 From: Komal Date: Tue, 3 Feb 2026 15:25:53 +0530 Subject: [PATCH 33/37] bug fix for empty subscription --- src/powershell/tests/Test-Assessment.25539.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 6eb908193..ffafbe2a4 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -71,7 +71,7 @@ function Test-Assessment-25539 { throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" } - $subscriptionsContent = $subscriptionsResponse.Content + $subscriptionsContent = $subscriptionsResponse.Content.Value if (-not $subscriptionsContent) { Add-ZtTestResultDetail -SkippedBecause NotSupported return From 70b916909cad729719c16f065a45e4f89a57b6a7 Mon Sep 17 00:00:00 2001 From: Komal Date: Wed, 4 Feb 2026 11:12:02 +0530 Subject: [PATCH 34/37] improve error handling --- .../tests/Test-Assessment.25539.ps1 | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index ffafbe2a4..4f676f625 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -71,12 +71,7 @@ function Test-Assessment-25539 { throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" } - $subscriptionsContent = $subscriptionsResponse.Content.Value - if (-not $subscriptionsContent) { - Add-ZtTestResultDetail -SkippedBecause NotSupported - return - } - + $subscriptionsContent = $subscriptionsResponse.Content $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value } catch { @@ -88,7 +83,15 @@ function Test-Assessment-25539 { $results = @() foreach ($sub in $subscriptions) { - Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction SilentlyContinue | Out-Null + + # Switch subscription context + try { + Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction Stop | Out-Null + } + catch { + Write-PSFMessage "Unable to switch to subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + continue + } # Query Azure Firewall Policies try { @@ -103,7 +106,8 @@ function Test-Assessment-25539 { } if ($policyResponse.StatusCode -ge 400) { - throw "Firewall policies request failed with status code $($policyResponse.StatusCode)" + Write-PSFMessage "Firewall policies request failed with status code $($policyResponse.StatusCode)" -Tag Firewall -Level Warning + continue } $policyResponseContent = $policyResponse.Content @@ -134,7 +138,8 @@ function Test-Assessment-25539 { } if ($detailResponse.StatusCode -ge 400) { - throw "Firewall policy details request failed with status code $($detailResponse.StatusCode)" + Write-PSFMessage "Firewall policy details request failed with status code $($detailResponse.StatusCode)" -Tag Firewall -Level Warning + continue } $detailResponseContent = $detailResponse.Content @@ -155,20 +160,20 @@ function Test-Assessment-25539 { foreach ($policyResource in $detailedPolicies) { # Skip if policy is missing required properties - if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.Properties) { + if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.properties) { Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose continue } # Skip if SKU tier is not Premium - if ($policyResource.Properties.sku.tier -ne 'Premium') { + if ($policyResource.properties.sku.tier -ne 'Premium') { Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose continue } # Get intrusion detection mode - if not configured, it's disabled by default (FAIL) - $idMode = if ($policyResource.Properties.intrusionDetection) { - $policyResource.Properties.intrusionDetection.mode + $idMode = if ($policyResource.properties.intrusionDetection) { + $policyResource.properties.intrusionDetection.mode } else { 'Off' } From b1c2f82a1b5b69892dfd77b22f1b8bd649d2ae52 Mon Sep 17 00:00:00 2001 From: komalp2025 Date: Tue, 3 Feb 2026 15:44:47 +0530 Subject: [PATCH 35/37] Update src/powershell/tests/Test-Assessment.25539.md fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25539.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md index 67c42cb1b..0bba2c3eb 100644 --- a/src/powershell/tests/Test-Assessment.25539.md +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -6,7 +6,7 @@ If this check does not pass, it means that the Intrusion Detection and Preventio **Remediation action** -- Please check the IDPS section of this article for guidance on how to enable Intrusion Deletion and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. +- Please check the IDPS section of this article for guidance on how to enable Intrusion Detection and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. - [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) From 525b49d86d8153969f74ce8b03f077a686708e3e Mon Sep 17 00:00:00 2001 From: komalp2025 Date: Tue, 3 Feb 2026 15:55:13 +0530 Subject: [PATCH 36/37] Update src/powershell/tests/Test-Assessment.25539.ps1 removing step 3 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25539.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index 4f676f625..d3e6d929f 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -125,7 +125,7 @@ function Test-Assessment-25539 { if (-not $policies) { continue } - # Step 3: Get individual firewall policy details + # Get individual firewall policy details $detailedPolicies = @() foreach ($policyResource in $policies) { try { From 896990491ecfbfd59649005f482590dc87e867e6 Mon Sep 17 00:00:00 2001 From: Komal Date: Thu, 5 Feb 2026 11:20:07 +0530 Subject: [PATCH 37/37] update skip reason --- src/powershell/tests/Test-Assessment.25539.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 index d3e6d929f..be9599795 100644 --- a/src/powershell/tests/Test-Assessment.25539.ps1 +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -46,7 +46,7 @@ function Test-Assessment-25539 { Write-ZtProgress -Activity $activity -Status 'Checking Azure environment' 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 + Add-ZtTestResultDetail -SkippedBecause NotApplicable return } @@ -62,13 +62,15 @@ function Test-Assessment-25539 { $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop if ($subscriptionsResponse.StatusCode -eq 403) { - Write-PSFMessage 'The signed in user does not have access to check subscriptions.' -Level Verbose + Write-PSFMessage 'The signed in user does not have access to check subscriptions.' -Tag Firewall -Level Warning Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } if ($subscriptionsResponse.StatusCode -ge 400) { - throw "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" + Write-PSFMessage "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" -Tag Firewall -Level Warning + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return } $subscriptionsContent = $subscriptionsResponse.Content @@ -203,7 +205,7 @@ function Test-Assessment-25539 { # If no Premium firewall policies found, skip the test if ($results.Count -eq 0) { Write-PSFMessage 'No Azure Firewall Premium policies found to evaluate.' -Tag Firewall -Level Verbose - Add-ZtTestResultDetail -SkippedBecause NotSupported + Add-ZtTestResultDetail -SkippedBecause NotApplicable return }