Skip to content

Commit 899e1d5

Browse files
authored
Merge pull request #922 from microsoft/test-26887-fresh
Network - 26887 - Diagnostic logging is enabled in Azure Firewall
2 parents e8624b5 + 6203efa commit 899e1d5

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Azure Firewall processes all inbound and outbound network traffic for protected workloads, making it a critical control point for network security monitoring. When diagnostic logging is not enabled, security operations teams lose visibility into traffic patterns, denied connection attempts, threat intelligence matches, and IDPS signature detections. A threat actor who gains initial access to an environment can move laterally through the network without detection because no firewall logs are being captured or analyzed. The absence of logging prevents correlation of network events with other security telemetry, eliminating the ability to construct attack timelines during incident investigations. Furthermore, compliance frameworks such as PCI-DSS, HIPAA, and SOC 2 require organizations to maintain audit logs of network security events, and the lack of firewall diagnostic logging creates audit failures. Azure Firewall provides multiple log categories including application rule logs, network rule logs, NAT rule logs, threat intelligence logs, IDPS signature logs, and DNS proxy logs, all of which must be routed to a destination such as Log Analytics, Storage Account, or Event Hub to enable security monitoring and forensic analysis.
2+
3+
**Remediation action**
4+
5+
Create a Log Analytics workspace for storing Azure Firewall logs
6+
- [Create a Log Analytics workspace](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/quick-create-workspace)
7+
8+
Configure diagnostic settings for Azure Firewall to enable log collection
9+
- [Create diagnostic settings in Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/create-diagnostic-settings)
10+
11+
Enable structured logs (resource-specific mode) for improved query performance and cost optimization
12+
- [Azure Firewall structured logs](https://learn.microsoft.com/en-us/azure/firewall/monitor-firewall#structured-azure-firewall-logs)
13+
14+
Use Azure Firewall Workbook for visualizing and analyzing firewall logs
15+
- [Azure Firewall Workbook](https://learn.microsoft.com/en-us/azure/firewall/firewall-workbook)
16+
17+
Monitor Azure Firewall metrics and logs for security operations
18+
- [Monitor Azure Firewall](https://learn.microsoft.com/en-us/azure/firewall/monitor-firewall)
19+
20+
<!--- Results --->
21+
%TestResult%
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
<#
2+
.SYNOPSIS
3+
Validates that diagnostic logging is enabled for Azure Firewall.
4+
5+
.DESCRIPTION
6+
This test evaluates diagnostic settings for Azure Firewall resources to ensure
7+
log categories are enabled with a valid destination configured (Log Analytics,
8+
Storage Account, or Event Hub).
9+
10+
.NOTES
11+
Test ID: 26887
12+
Category: Azure Network Security
13+
Required APIs: Azure Management REST API (subscriptions, firewalls, diagnostic settings)
14+
#>
15+
16+
function Test-Assessment-26887 {
17+
18+
[ZtTest(
19+
Category = 'Azure Network Security',
20+
ImplementationCost = 'Low',
21+
MinimumLicense = ('Azure_Firewall_Standard', 'Azure_Firewall_Premium'),
22+
Pillar = 'Network',
23+
RiskLevel = 'High',
24+
SfiPillar = 'Monitor and detect cyberthreats',
25+
TenantType = ('Workforce'),
26+
TestId = 26887,
27+
Title = 'Diagnostic logging is enabled in Azure Firewall',
28+
UserImpact = 'Low'
29+
)]
30+
[CmdletBinding()]
31+
param()
32+
33+
#region Data Collection
34+
35+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
36+
$activity = 'Evaluating Azure Firewall diagnostic logging configuration'
37+
38+
# Check if connected to Azure
39+
Write-ZtProgress -Activity $activity -Status 'Checking Azure connection'
40+
41+
$azContext = Get-AzContext -ErrorAction SilentlyContinue
42+
if (-not $azContext) {
43+
Write-PSFMessage 'Not connected to Azure.' -Level Warning
44+
Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure
45+
return
46+
}
47+
48+
# Check the supported environment
49+
Write-ZtProgress -Activity $activity -Status 'Checking Azure environment'
50+
51+
if ($azContext.Environment.Name -ne 'AzureCloud') {
52+
Write-PSFMessage 'This test is only applicable to the AzureCloud environment.' -Tag Test -Level VeryVerbose
53+
Add-ZtTestResultDetail -SkippedBecause NotSupported
54+
return
55+
}
56+
57+
# Check Azure access token
58+
try {
59+
$accessToken = Get-AzAccessToken -AsSecureString -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
60+
}
61+
catch {
62+
Write-PSFMessage $_.Exception.Message -Tag Test -Level Error
63+
}
64+
65+
if (-not $accessToken) {
66+
Write-PSFMessage 'Azure authentication token not found.' -Tag Test -Level Warning
67+
Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure
68+
return
69+
}
70+
71+
# Q1 & Q2: Query Azure Firewalls using Azure Resource Graph
72+
Write-ZtProgress -Activity $activity -Status 'Querying Azure Firewalls via Resource Graph'
73+
74+
$argQuery = @"
75+
resources
76+
| where type =~ 'microsoft.network/azurefirewalls'
77+
| where properties.provisioningState =~ 'Succeeded'
78+
| join kind=leftouter (
79+
resourcecontainers
80+
| where type =~ 'microsoft.resources/subscriptions'
81+
| project subscriptionName=name, subscriptionId
82+
) on subscriptionId
83+
| project
84+
FirewallName=name,
85+
FirewallId=id,
86+
Location=location,
87+
SkuName=tostring(properties.sku.name),
88+
SkuTier=tostring(properties.sku.tier),
89+
SubscriptionId=subscriptionId,
90+
SubscriptionName=subscriptionName
91+
"@
92+
93+
$allFirewalls = @()
94+
try {
95+
$allFirewalls = @(Invoke-ZtAzureResourceGraphRequest -Query $argQuery)
96+
Write-PSFMessage "ARG Query returned $($allFirewalls.Count) Azure Firewall(s)" -Tag Test -Level VeryVerbose
97+
}
98+
catch {
99+
Write-PSFMessage "Azure Resource Graph query failed: $($_.Exception.Message)" -Tag Test -Level Warning
100+
Add-ZtTestResultDetail -SkippedBecause NotSupported
101+
return
102+
}
103+
104+
# Check if any Azure Firewall resources exist
105+
if ($allFirewalls.Count -eq 0) {
106+
Write-PSFMessage 'No Azure Firewall resources found.' -Tag Test -Level VeryVerbose
107+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
108+
return
109+
}
110+
111+
# Q3: Get diagnostic settings for each Azure Firewall
112+
Write-ZtProgress -Activity $activity -Status 'Querying diagnostic settings'
113+
114+
$evaluationResults = @()
115+
116+
foreach ($firewall in $allFirewalls) {
117+
$firewallId = $firewall.FirewallId
118+
$firewallName = $firewall.FirewallName
119+
$firewallLocation = $firewall.Location
120+
$firewallSku = "$($firewall.SkuName)/$($firewall.SkuTier)"
121+
122+
# Q3: Query diagnostic settings using Invoke-ZtAzureRequest
123+
$diagPath = $firewallId + '/providers/Microsoft.Insights/diagnosticSettings?api-version=2021-05-01-preview'
124+
125+
$diagSettings = @()
126+
try {
127+
$diagSettings = @(Invoke-ZtAzureRequest -Path $diagPath)
128+
}
129+
catch {
130+
Write-PSFMessage "Error querying diagnostic settings for $firewallName : $_" -Level Warning
131+
}
132+
133+
# Evaluate diagnostic settings
134+
$hasValidDiagSetting = $false
135+
$allDestinationTypes = @()
136+
$enabledCategories = @()
137+
$diagSettingNames = @()
138+
139+
foreach ($setting in $diagSettings) {
140+
$workspaceId = $setting.properties.workspaceId
141+
$storageAccountId = $setting.properties.storageAccountId
142+
$eventHubAuthRuleId = $setting.properties.eventHubAuthorizationRuleId
143+
144+
# Check if destination is configured
145+
$hasDestination = $workspaceId -or $storageAccountId -or $eventHubAuthRuleId
146+
147+
if ($hasDestination) {
148+
# Determine destination type
149+
$destTypes = @()
150+
if ($workspaceId) { $destTypes += 'Log Analytics' }
151+
if ($storageAccountId) { $destTypes += 'Storage' }
152+
if ($eventHubAuthRuleId) { $destTypes += 'Event Hub' }
153+
154+
# Collect all enabled log categories from this setting
155+
$logs = $setting.properties.logs
156+
$settingEnabledCategories = @()
157+
foreach ($log in $logs) {
158+
if ($log.enabled) {
159+
# Handle both category and categoryGroup (per spec)
160+
$categoryName = if ($log.category) { $log.category } else { $log.categoryGroup }
161+
if ($categoryName) {
162+
$settingEnabledCategories += $categoryName
163+
}
164+
}
165+
}
166+
167+
# If this setting has destination AND enabled logs, it's valid
168+
if ($settingEnabledCategories.Count -gt 0) {
169+
$hasValidDiagSetting = $true
170+
$diagSettingNames += $setting.name
171+
$allDestinationTypes += $destTypes
172+
$enabledCategories += $settingEnabledCategories
173+
}
174+
}
175+
}
176+
177+
# Deduplicate enabled categories and destination types (multiple settings may have same values)
178+
$enabledCategories = $enabledCategories | Select-Object -Unique
179+
$destinationType = if ($allDestinationTypes.Count -gt 0) { ($allDestinationTypes | Select-Object -Unique) -join ', ' } else { 'None' }
180+
181+
$status = if ($hasValidDiagSetting) { 'Pass' } else { 'Fail' }
182+
183+
$evaluationResults += [PSCustomObject]@{
184+
SubscriptionId = $firewall.SubscriptionId
185+
SubscriptionName = $firewall.SubscriptionName
186+
FirewallName = $firewallName
187+
FirewallId = $firewallId
188+
Location = $firewallLocation
189+
Sku = $firewallSku
190+
DiagnosticSettingCount = $diagSettings.Count
191+
DiagnosticSettingName = ($diagSettingNames | Select-Object -Unique) -join ', '
192+
DestinationType = $destinationType
193+
EnabledCategories = $enabledCategories -join ', '
194+
Status = $status
195+
}
196+
}
197+
198+
#endregion Data Collection
199+
200+
#region Assessment Logic
201+
202+
$passedItems = $evaluationResults | Where-Object { $_.Status -eq 'Pass' }
203+
$failedItems = $evaluationResults | Where-Object { $_.Status -eq 'Fail' }
204+
205+
$passed = ($failedItems.Count -eq 0) -and ($passedItems.Count -gt 0)
206+
207+
if ($passed) {
208+
$testResultMarkdown = "✅ Diagnostic logging is enabled for Azure Firewall with active log collection configured.`n`n%TestResult%"
209+
}
210+
else {
211+
$testResultMarkdown = "❌ Diagnostic logging is not enabled for Azure Firewall, preventing security monitoring and threat detection.`n`n%TestResult%"
212+
}
213+
214+
#endregion Assessment Logic
215+
216+
#region Report Generation
217+
218+
# Portal link variables
219+
$portalFirewallBrowseLink = 'https://portal.azure.com/#browse/Microsoft.Network%2FazureFirewalls'
220+
$portalSubscriptionBaseLink = 'https://portal.azure.com/#resource/subscriptions'
221+
$portalResourceBaseLink = 'https://portal.azure.com/#resource'
222+
223+
$mdInfo = "`n## [Azure Firewall diagnostic logging status]($portalFirewallBrowseLink)`n`n"
224+
225+
# Azure Firewall Status table
226+
if ($evaluationResults.Count -gt 0) {
227+
$tableRows = ""
228+
$formatTemplate = @'
229+
| Subscription | Firewall name | Location | Diagnostic settings count | Destination configured | Enabled log categories | Status |
230+
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
231+
{0}
232+
233+
'@
234+
235+
# Limit display to first 5 items if there are many firewalls
236+
$maxItemsToDisplay = 5
237+
$displayResults = $evaluationResults
238+
$hasMoreItems = $false
239+
if ($evaluationResults.Count -gt $maxItemsToDisplay) {
240+
$displayResults = $evaluationResults | Select-Object -First $maxItemsToDisplay
241+
$hasMoreItems = $true
242+
}
243+
244+
foreach ($result in $displayResults) {
245+
$subscriptionLink = "[$(Get-SafeMarkdown $result.SubscriptionName)]($portalSubscriptionBaseLink/$($result.SubscriptionId)/overview)"
246+
$firewallLink = "[$(Get-SafeMarkdown $result.FirewallName)]($portalResourceBaseLink$($result.FirewallId)/diagnostics)"
247+
$diagCount = $result.DiagnosticSettingCount
248+
$destConfigured = if ($result.DestinationType -eq 'None') { 'No' } else { 'Yes' }
249+
$enabledCategories = if ($result.DiagnosticSettingCount -eq 0) {
250+
'No diagnostic settings'
251+
} elseif ($result.EnabledCategories) {
252+
$result.EnabledCategories
253+
} else {
254+
'None'
255+
}
256+
$statusText = if ($result.Status -eq 'Pass') { '✅ Pass' } else { '❌ Fail' }
257+
258+
$tableRows += "| $subscriptionLink | $firewallLink | $($result.Location) | $diagCount | $destConfigured | $enabledCategories | $statusText |`n"
259+
}
260+
261+
# Add note if more items exist
262+
if ($hasMoreItems) {
263+
$remainingCount = $evaluationResults.Count - $maxItemsToDisplay
264+
$tableRows += "`n... and $remainingCount more. [View all Azure Firewalls in the portal]($portalFirewallBrowseLink)`n"
265+
}
266+
267+
$mdInfo += $formatTemplate -f $tableRows
268+
}
269+
270+
# Summary
271+
$mdInfo += "**Summary:**`n`n"
272+
$mdInfo += "- Total Azure Firewalls evaluated: $($evaluationResults.Count)`n"
273+
$mdInfo += "- Firewalls with diagnostic logging enabled: $($passedItems.Count)`n"
274+
$mdInfo += "- Firewalls without diagnostic logging: $($failedItems.Count)`n"
275+
276+
# Replace the placeholder with detailed information
277+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
278+
279+
#endregion Report Generation
280+
281+
$params = @{
282+
TestId = '26887'
283+
Title = 'Diagnostic logging is enabled in Azure Firewall'
284+
Status = $passed
285+
Result = $testResultMarkdown
286+
}
287+
288+
Add-ZtTestResultDetail @params
289+
}

0 commit comments

Comments
 (0)