Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/powershell/tests/Test-Assessment.25535.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Azure Firewall is a cloud-native network security service that provides centralized inspection, logging, and enforcement for network traffic flowing between application workloads and external destinations. Routing outbound traffic through Azure Firewall enables organizations to apply consistent security controls such as threat intelligence filtering, intrusion detection and prevention, TLS inspection, and egress policy enforcement. In a secure network architecture, outbound traffic from workloads hosted in Azure virtual networks should be explicitly routed through Azure Firewall before reaching the internet or external services. VNET integrated workloads include VMs, AKS Node Pools, AKS Pods, App Service (VNet Integration Route All), Functions in VNet. This is typically achieved by configuring routing so that outbound traffic from workload subnets uses Azure Firewall as the next hop. Without this routing in place, outbound traffic may bypass the firewall entirely, reducing visibility and allowing traffic to leave the environment without inspection or policy enforcement.This check verifies that outbound traffic from in-scope workloads is routed through Azure Firewall by validating that the effective network routes direct outbound traffic to the firewall’s private IP address. If outbound traffic is not routed through Azure Firewall, the check fails because traffic may bypass centralized security controls, increasing the risk of data exfiltration, command-and-control communication, and undetected malicious activity.

**Remediation action**

- [Deploy and configure Azure Firewall using the Azure portal](https://learn.microsoft.com/en-us/azure/firewall/tutorial-firewall-deploy-portal#configure-routing)
- [Control outbound traffic with Azure Firewall ](https://learn.microsoft.com/en-us/azure/app-service/network-secure-outbound-traffic-azure-firewall)

<!--- Results --->
%TestResult%
236 changes: 236 additions & 0 deletions src/powershell/tests/Test-Assessment.25535.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
function Test-Assessment-25535 {
[ZtTest(
Category = 'Azure Network Security',
ImplementationCost = 'Medium',
MinimumLicense = ('Azure_Firewall_Basic', 'Azure_Firewall_Standard', 'Azure_Firewall_Premium'),
Pillar = 'Network',
RiskLevel = 'High',
SfiPillar = 'Protect networks',
TenantType = ('Workforce', 'External'),
TestId = 25535,
Title = 'Outbound traffic from VNET integrated workloads is routed through Azure Firewall',
UserImpact = 'Low'
)]
[CmdletBinding()]
param()

Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

if ((Get-MgContext).Environment -ne 'Global') {
Write-PSFMessage "This test is only applicable to the Global environment." -Tag Test -Level VeryVerbose
return
}

#region Azure Connection Verification
try {
$accessToken = Get-AzAccessToken -AsSecureString -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
catch {
Write-PSFMessage $_.Exception.Message -Tag Test -Level Error
}

if (-not $accessToken) {
Write-PSFMessage "Azure authentication token not found." -Tag Test -Level Warning
Add-ZtTestResultDetail -SkippedBecause NoAzureAccess
return
}
#endregion Azure Connection Verification

#region Data Collection
$azAccessToken = $accessToken.Token
$resourceManagementUrl = (Get-AzContext).Environment.ResourceManagerUrl
$subscriptions = Get-AzSubscription

$firewalls = @()
$nicFindings = @()

foreach ($sub in $subscriptions) {

Set-AzContext -SubscriptionId $sub.Id | Out-Null
$subId = $sub.Id

# Step 1: List Azure Firewalls
$fwListUri = $resourceManagementUrl.TrimEnd('/') +
"/subscriptions/$subId/providers/Microsoft.Network/azureFirewalls?api-version=2025-03-01"

try {
$fwResp = Invoke-WebRequest -Uri $fwListUri -Authentication Bearer -Token $azAccessToken -ErrorAction Stop
}
catch {
Write-PSFMessage "Unable to list Azure Firewalls in subscription $($sub.Name)." -Tag Test -Level Warning
continue
}

$fwItems = ($fwResp.Content | ConvertFrom-Json).value
if (-not $fwItems) {
continue
}

# Step 2: Resolve Firewall Private IPs
foreach ($fw in $fwItems) {

$fwDetailUri = $resourceManagementUrl.TrimEnd('/') +
"$($fw.id)?api-version=2025-03-01"

try {
$fwDetailResp = Invoke-WebRequest -Uri $fwDetailUri -Authentication Bearer -Token $azAccessToken -ErrorAction Stop
$fwDetail = $fwDetailResp.Content | ConvertFrom-Json
}
catch {
continue
}

foreach ($ipconfig in $fwDetail.properties.ipConfigurations) {
if ($ipconfig.properties.privateIPAddress) {
$firewalls += [PSCustomObject]@{
FirewallName = $fwDetail.name
FirewallId = $fwDetail.id
PrivateIP = $ipconfig.properties.privateIPAddress
SubscriptionId = $subId
}
}
}
}

if ($firewalls.Count -eq 0) {
continue
}

# Step 3: List NICs
$nicListUri = $resourceManagementUrl.TrimEnd('/') +
"/subscriptions/$subId/providers/Microsoft.Network/networkInterfaces?api-version=2025-03-01"

$nicResp = Invoke-WebRequest -Uri $nicListUri -Authentication Bearer -Token $azAccessToken
$nics = ($nicResp.Content | ConvertFrom-Json).value

foreach ($nic in $nics) {

foreach ($ipconfig in $nic.properties.ipConfigurations) {

$subnetId = $ipconfig.properties.subnet.id
if ($subnetId -match 'AzureFirewallSubnet|GatewaySubnet|AzureBastionSubnet') {
continue
}

# Step 4: Effective Route Table (ASYNC)
$rg = ($nic.id -split '/')[4]
$nicName = $nic.name

$ertUri = $resourceManagementUrl.TrimEnd('/') +
"/subscriptions/$subId/resourceGroups/$rg/providers/Microsoft.Network/networkInterfaces/$nicName/effectiveRouteTable?api-version=2025-03-01"

try {
$ertStart = Invoke-WebRequest `
-Uri $ertUri `
-Authentication Bearer `
-Token $azAccessToken `
-Method Post

$operationUri = $ertStart.Headers['Location'][0]

$retryAfter = if ($ertStart.Headers['Retry-After']) {
[int]$ertStart.Headers['Retry-After'][0]
}
else {
5
}

do {
Start-Sleep -Seconds $retryAfter

$ertPoll = Invoke-WebRequest `
-Uri $operationUri `
-Authentication Bearer `
-Token $azAccessToken `
-Method Get

} while ($ertPoll.StatusCode -eq 202)

$routes = ($ertPoll.Content | ConvertFrom-Json).value
}
catch {
continue
}

$defaultRoute = $routes | Where-Object {
$_.state -eq 'Active' -and
$_.source -eq 'User' -and
$_.addressPrefix -contains '0.0.0.0/0'
} | Select-Object -First 1

# No user-defined default route → FAIL
if (-not $defaultRoute) {
$nicFindings += [PSCustomObject]@{
NicName = $nic.name
NicId = $nic.id
NextHopType = 'Internet'
NextHopIp = ''
IsCompliant = $false
}
continue
}

# Match firewall private IP
$fwMatch = $firewalls | Where-Object {
$defaultRoute.nextHopIpAddress -contains $_.PrivateIP
} | Select-Object -First 1

$nicFindings += [PSCustomObject]@{
FirewallName = $fwMatch.FirewallName
FirewallId = $fwMatch.FirewallId
FirewallPrivateIp = $fwMatch.PrivateIP

NicName = $nic.name
NicId = $nic.id

RouteSource = $defaultRoute.source
RouteState = $defaultRoute.state
AddressPrefix = ($defaultRoute.addressPrefix -join ',')
NextHopType = $defaultRoute.nextHopType
NextHopIpAddress = ($defaultRoute.nextHopIpAddress -join ',')

IsCompliant = ($fwMatch -ne $null)
}
}
}
}
#endregion Data Collection

#region Assessment Logic
if ($nicFindings.Count -eq 0) {
Add-ZtTestResultDetail -SkippedBecause NoResults
return
}

$nonCompliant = $nicFindings | Where-Object { -not $_.IsCompliant }
$passed = ($nonCompliant.Count -eq 0)

if ($passed) {
$testResultMarkdown = "Outbound traffic is routed through Azure Firewall private IP using a user-defined default route.`n`n%TestResult%"
}
else {
$testResultMarkdown = "Outbound traffic is not routed through Azure Firewall. The effective route for outbound traffic does not forward 0.0.0.0/0 to the Azure Firewall private IP.`n`n%TestResult%"
}
#endregion Assessment Logic

#region Result Reporting
$mdInfo = "## Outbound traffic routing evidence`n`n"
$mdInfo += "| Azure Firewall | Firewall Private IP | Network Interface | Source | State | Address Prefix | Next Hop Type | Next Hop IP | Status |`n"
$mdInfo += "| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |`n"

foreach ($item in $nicFindings | Sort-Object NicName) {
$icon = if ($item.IsCompliant) {
'✅'
}
else {
'❌'
}
$safeName = Get-SafeMarkdown -Text $item.NicName
$mdInfo += "| $icon [$safeName](https://portal.azure.com/#@/resource$($item.NicId)) | $($item.NextHopType) | $($item.NextHopIp) | $icon |`n"
}

$testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo

Add-ZtTestResultDetail -TestId '25535' -Status $passed -Result $testResultMarkdown
#endregion Result Reporting
}