-
Notifications
You must be signed in to change notification settings - Fork 123
Network - 25539 IDPS Inspection is Enabled in Deny Mode on Azure Firewall #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
komalp2025
wants to merge
39
commits into
main
from
Network-25339-IDPS-Inspection-is-Enabled-in-Deny-Mode-on-Azure-Firewall-2
Closed
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
1515da3
add 25539
komalp2025 7f6248f
Add test 25539
komalp2025 b4e0d14
updated user facing message
komalp2025 59aa9d5
update metadata
komalp2025 47fbbf8
update assessment logic
komalp2025 71b9791
add skip logic
komalp2025 7adbf3e
add skip logic
komalp2025 c899ac4
skip if policy missing
komalp2025 0dec233
add skip reason
komalp2025 083f147
fix output and fail condition
komalp2025 972e61e
add new line
komalp2025 ef5d286
added skip before return
komalp2025 d14f706
bug fix for empty subscription
komalp2025 10082fa
Update src/powershell/tests/Test-Assessment.25539.md
komalp2025 d43a0f5
Update src/powershell/tests/Test-Assessment.25539.ps1
komalp2025 c115e98
improve error handling
komalp2025 cf0680b
Merge branch 'Network-25339-IDPS-Inspection-is-Enabled-in-Deny-Mode-o…
komalp2025 a1b12ae
Added helper function for 25377 (#849)
ashwinikarke 24d65e5
Data 35039 Copilot Communication Compliance Monitoring Configured (#776)
komalp2025 3e10635
Added NotApplicable reason to Get-ZtSkippedReason function (#852)
praneeth-0000 7fce508
Bump @modelcontextprotocol/sdk from 1.24.0 to 1.26.0 (#859)
dependabot[bot] ef13d39
add 25539
komalp2025 72a2ddf
Add test 25539
komalp2025 55bef42
updated user facing message
komalp2025 14cbbf6
update metadata
komalp2025 9e04788
update assessment logic
komalp2025 cda9e67
add skip logic
komalp2025 b5c5709
add skip logic
komalp2025 2e91603
skip if policy missing
komalp2025 9a37c4f
add skip reason
komalp2025 b679385
fix output and fail condition
komalp2025 595a0a0
add new line
komalp2025 1f2d63b
added skip before return
komalp2025 3c3f8e7
bug fix for empty subscription
komalp2025 70b9169
improve error handling
komalp2025 b1c2f82
Update src/powershell/tests/Test-Assessment.25539.md
komalp2025 525b49d
Update src/powershell/tests/Test-Assessment.25539.ps1
komalp2025 8969904
update skip reason
komalp2025 2d4add8
resolve conflict
komalp2025 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 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 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) | ||
| <!--- Results ---> | ||
| %TestResult% |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,265 @@ | ||
| <# | ||
| .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','External'), | ||
| 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 '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 NotApplicable | ||
| return | ||
| } | ||
|
|
||
| Write-ZtProgress ` | ||
| -Activity $activity ` | ||
| -Status 'Enumerating Firewall Policies' | ||
|
|
||
| # Query subscriptions using REST API | ||
| $resourceManagerUrl = $azContext.Environment.ResourceManagerUrl.TrimEnd('/') | ||
| $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2025-03-01" | ||
|
|
||
| try { | ||
| $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.' -Tag Firewall -Level Warning | ||
| Add-ZtTestResultDetail -SkippedBecause NoAzureAccess | ||
| return | ||
| } | ||
|
|
||
| if ($subscriptionsResponse.StatusCode -ge 400) { | ||
| Write-PSFMessage "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" -Tag Firewall -Level Warning | ||
| Add-ZtTestResultDetail -SkippedBecause NoAzureAccess | ||
| return | ||
| } | ||
|
|
||
| $subscriptionsContent = $subscriptionsResponse.Content | ||
| $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value | ||
| } | ||
| catch { | ||
| Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning | ||
| Add-ZtTestResultDetail -SkippedBecause NoAzureAccess | ||
| return | ||
| } | ||
|
|
||
| $results = @() | ||
|
|
||
| foreach ($sub in $subscriptions) { | ||
|
|
||
| # 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 { | ||
| $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 | ||
|
|
||
| if ($policyResponse.StatusCode -eq 403) { | ||
| Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning | ||
| continue | ||
| } | ||
|
|
||
| if ($policyResponse.StatusCode -ge 400) { | ||
| Write-PSFMessage "Firewall policies request failed with status code $($policyResponse.StatusCode)" -Tag Firewall -Level Warning | ||
| continue | ||
| } | ||
|
|
||
| $policyResponseContent = $policyResponse.Content | ||
| if (-not $policyResponseContent) { | ||
| Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning | ||
| continue | ||
| } | ||
|
|
||
| $policies = ($policyResponseContent | ConvertFrom-Json).value | ||
| } | ||
| catch { | ||
| Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning | ||
| continue | ||
| } | ||
|
|
||
| if (-not $policies) { continue } | ||
|
|
||
| # Get individual firewall policy details | ||
| $detailedPolicies = @() | ||
| foreach ($policyResource in $policies) { | ||
| try { | ||
| $detailUri = "$resourceManagerUrl$($policyResource.id)?api-version=2025-03-01" | ||
| $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) { | ||
| Write-PSFMessage "Firewall policy details request failed with status code $($detailResponse.StatusCode)" -Tag Firewall -Level Warning | ||
| continue | ||
| } | ||
|
|
||
| $detailResponseContent = $detailResponse.Content | ||
| if (-not $detailResponseContent) { | ||
| Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning | ||
| continue | ||
| } | ||
|
|
||
| $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 | ||
| } | ||
| } | ||
|
|
||
| # 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 | ||
| 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 | ||
| } | ||
|
|
||
| # Get intrusion detection mode - if not configured, it's disabled by default (FAIL) | ||
| $idMode = if ($policyResource.properties.intrusionDetection) { | ||
| $policyResource.properties.intrusionDetection.mode | ||
| } else { | ||
| 'Off' | ||
| } | ||
| # Map intrusion detection mode to user-friendly display values | ||
| $detectionModeDisplay = switch ($idMode) { | ||
| 'Deny' { 'Alert and Deny' } | ||
| 'Alert' { 'Alert Only' } | ||
| 'Off' { 'Disabled' } | ||
| } | ||
|
|
||
| $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 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 NotApplicable | ||
| return | ||
| } | ||
|
|
||
| $failedPolicies = @($results | Where-Object { -not $_.Passed }) | ||
| $passed = $failedPolicies.Count -eq 0 | ||
|
|
||
| if ($passed) { | ||
| $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 Azure Firewall policies.`n`n%TestResult%" | ||
| } | ||
| #endregion Assessment Logic | ||
|
|
||
| #region Report Generation | ||
| $reportTitle = "Firewall policies" | ||
| $tableRows = "" | ||
| $mdInfo = "" | ||
|
|
||
| if ($results.Count -gt 0) { | ||
| # Create a here-string with format placeholders {0}, {1}, etc. | ||
| $formatTemplate = @' | ||
|
|
||
| ## {0} | ||
|
|
||
| | Policy name | Subscription name | 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 { '❌' } | ||
| $resultText = "$icon $($item.IntrusionDetectionMode)" | ||
| $tableRows += "| $policyMd | $subMd | $resultText |`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' | ||
| Status = $passed | ||
| Result = $testResultMarkdown | ||
| } | ||
|
|
||
| Add-ZtTestResultDetail @params | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.