Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1515da3
add 25539
komalp2025 Jan 23, 2026
7f6248f
Add test 25539
komalp2025 Jan 29, 2026
b4e0d14
updated user facing message
komalp2025 Jan 29, 2026
59aa9d5
update metadata
komalp2025 Jan 29, 2026
47fbbf8
update assessment logic
komalp2025 Jan 30, 2026
71b9791
add skip logic
komalp2025 Feb 3, 2026
7adbf3e
add skip logic
komalp2025 Feb 3, 2026
c899ac4
skip if policy missing
komalp2025 Feb 3, 2026
0dec233
add skip reason
komalp2025 Feb 3, 2026
083f147
fix output and fail condition
komalp2025 Feb 3, 2026
972e61e
add new line
komalp2025 Feb 3, 2026
ef5d286
added skip before return
komalp2025 Feb 3, 2026
d14f706
bug fix for empty subscription
komalp2025 Feb 3, 2026
10082fa
Update src/powershell/tests/Test-Assessment.25539.md
komalp2025 Feb 3, 2026
d43a0f5
Update src/powershell/tests/Test-Assessment.25539.ps1
komalp2025 Feb 3, 2026
c115e98
improve error handling
komalp2025 Feb 4, 2026
cf0680b
Merge branch 'Network-25339-IDPS-Inspection-is-Enabled-in-Deny-Mode-o…
komalp2025 Feb 4, 2026
a1b12ae
Added helper function for 25377 (#849)
ashwinikarke Feb 4, 2026
24d65e5
Data 35039 Copilot Communication Compliance Monitoring Configured (#776)
komalp2025 Feb 4, 2026
3e10635
Added NotApplicable reason to Get-ZtSkippedReason function (#852)
praneeth-0000 Feb 5, 2026
7fce508
Bump @modelcontextprotocol/sdk from 1.24.0 to 1.26.0 (#859)
dependabot[bot] Feb 5, 2026
ef13d39
add 25539
komalp2025 Jan 23, 2026
72a2ddf
Add test 25539
komalp2025 Jan 29, 2026
55bef42
updated user facing message
komalp2025 Jan 29, 2026
14cbbf6
update metadata
komalp2025 Jan 29, 2026
9e04788
update assessment logic
komalp2025 Jan 30, 2026
cda9e67
add skip logic
komalp2025 Feb 3, 2026
b5c5709
add skip logic
komalp2025 Feb 3, 2026
2e91603
skip if policy missing
komalp2025 Feb 3, 2026
9a37c4f
add skip reason
komalp2025 Feb 3, 2026
b679385
fix output and fail condition
komalp2025 Feb 3, 2026
595a0a0
add new line
komalp2025 Feb 3, 2026
1f2d63b
added skip before return
komalp2025 Feb 3, 2026
3c3f8e7
bug fix for empty subscription
komalp2025 Feb 3, 2026
70b9169
improve error handling
komalp2025 Feb 4, 2026
b1c2f82
Update src/powershell/tests/Test-Assessment.25539.md
komalp2025 Feb 3, 2026
525b49d
Update src/powershell/tests/Test-Assessment.25539.ps1
komalp2025 Feb 3, 2026
8969904
update skip reason
komalp2025 Feb 5, 2026
2d4add8
resolve conflict
komalp2025 Feb 5, 2026
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
218 changes: 124 additions & 94 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/powershell/private/core/Add-ZtTestResultDetail.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
1 change: 1 addition & 0 deletions src/powershell/private/core/Get-ZtSkippedReason.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
}
82 changes: 82 additions & 0 deletions src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1
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
}
13 changes: 13 additions & 0 deletions src/powershell/tests/Test-Assessment.25539.md
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%
265 changes: 265 additions & 0 deletions src/powershell/tests/Test-Assessment.25539.ps1
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
}
Loading