Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
22 changes: 11 additions & 11 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
steps:

- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
steps:

- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
Expand Down Expand Up @@ -154,7 +154,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0

Expand Down Expand Up @@ -191,7 +191,7 @@ jobs:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Run PSRule analysis
uses: microsoft/ps-rule@46451b8f5258c41beb5ae69ed7190ccbba84112c # v2.9.0
Expand All @@ -202,7 +202,7 @@ jobs:
outputPath: reports/ps-rule-results.sarif

- name: Upload results to security tab
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
if: always()
with:
sarif_file: reports/ps-rule-results.sarif
Expand All @@ -225,15 +225,15 @@ jobs:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@4b5047945a44163b94642a1cecc0d93a3f428cc6 # v1.0.16
with:
directory-to-scan: .

- name: Upload results to security tab
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
if: always()
with:
sarif_file: devskim-results.sarif
Expand All @@ -256,18 +256,18 @@ jobs:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Initialize CodeQL
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
languages: 'csharp'

- name: Autobuild
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
id: codeql-analyze

- name: Upload results
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
steps:

- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0

Expand Down Expand Up @@ -59,7 +59,7 @@ jobs:
shell: pwsh

- name: Checkout gh-pages
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: refs/heads/gh-pages
path: site/
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: refs/heads/gh-pages

Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
- Data Explorer:
- Check that public network access is disabled by @BenjaminEngeset.
[#3114](https://github.com/Azure/PSRule.Rules.Azure/issues/3114)
- Event Hub:
- Check that zone redundancy is enabled for Event Hub namespaces in supported regions by @BenjaminEngeset.
[#3029](https://github.com/Azure/PSRule.Rules.Azure/issues/3029)
- Managed Grafana:
- Check that zone redundancy is enabled for Grafana workspaces in supported regions by @BenjaminEngeset.
[#3294](https://github.com/Azure/PSRule.Rules.Azure/issues/3294)
Expand Down
109 changes: 109 additions & 0 deletions docs/en/rules/Azure.EventHub.AvailabilityZone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
reviewed: 2025-11-19
severity: Important
pillar: Reliability
category: RE:05 Redundancy
resource: Event Hub
resourceType: Microsoft.EventHub/namespaces
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.EventHub.AvailabilityZone/
---

# Use zone redundant Event Hub namespaces

## SYNOPSIS

Use zone redundant Event Hub namespaces in supported regions to improve reliability.

## DESCRIPTION

Azure Event Hubs supports zone redundancy to provide enhanced resiliency and high availability.
When zone redundancy is enabled, Event Hubs automatically replicates namespace metadata
and event data across multiple availability zones within a region.

Availability zones are unique physical locations within an Azure region.
Each zone is made up of one or more datacenters equipped with independent power, cooling, and networking.
This physical separation protects your Event Hubs namespace from zone-level failures,
ensuring continuous availability even if an entire availability zone experiences an outage.

With zone redundancy enabled, Azure Event Hubs provides:

- Synchronous replication of metadata and events across zones.
- Continuous availability during zonal failures.
- Protection against datacenter-level disasters while maintaining low-latency access.

Zone redundancy must be configured when you create an Event Hub namespace by setting `zoneRedundant` to `true`.
This setting cannot be changed after the namespace is created.
Zone redundancy is only available in regions that support availability zones.

## RECOMMENDATION

Consider using using a minimum of Standard Event Hub namespaces configured with zone redundancy to improve workload resiliency.

## EXAMPLES

### Configure with Azure template

To deploy Event Hub namespaces that pass this rule:

- Set the `properties.zoneRedundant` property to `true`.

For example:

```json
{
"type": "Microsoft.EventHub/namespaces",
"apiVersion": "2024-01-01",
"name": "[parameters('name')]",
"location": "[parameters('location')]",
"sku": {
"name": "Standard",
"tier": "Standard"
},
"properties": {
"disableLocalAuth": true,
"minimumTlsVersion": "1.2",
"zoneRedundant": true
}
}
```

### Configure with Bicep

To deploy Event Hub namespaces that pass this rule:

- Set the `properties.zoneRedundant` property to `true`.

For example:

```bicep
resource eventHubNamespace 'Microsoft.EventHub/namespaces@2024-01-01' = {
name: name
location: location
sku: {
name: 'Standard'
tier: 'Standard'
}
properties: {
disableLocalAuth: true
minimumTlsVersion: '1.2'
zoneRedundant: true
}
}
```

<!-- external:avm avm/res/event-hub/namespace zoneRedundant -->

## NOTES

Availability zones is supported on Standard, Premium, and Dedicated tiers.

For the Dedicated tier, availability zones require a minimum of three capacity units (CUs).

## LINKS

- [RE:05 Redundancy](https://learn.microsoft.com/azure/well-architected/reliability/redundancy)
- [Reliability: Level 1](https://learn.microsoft.com/azure/well-architected/reliability/maturity-model?tabs=level1)
- [Architecture strategies for using availability zones and regions](https://learn.microsoft.com/azure/well-architected/reliability/regions-availability-zones)
- [Azure regions with availability zone support](https://learn.microsoft.com/azure/reliability/availability-zones-service-support)
- [High availability with Azure Event Hubs](https://learn.microsoft.com/azure/reliability/reliability-event-hubs)
- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.eventhub/namespaces)
14 changes: 14 additions & 0 deletions src/PSRule.Rules.Azure/rules/Azure.EventHub.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ Rule 'Azure.EventHub.Firewall' -Ref 'AZR-000422' -Type 'Microsoft.EventHub/names
}
}

# Synopsis: Use zone redundant Event Hub namespaces in supported regions to improve reliability.
Rule 'Azure.EventHub.AvailabilityZone' -Ref 'AZR-000532' -Type 'Microsoft.EventHub/namespaces' -Tag @{ release = 'GA'; ruleSet = '2025_12'; 'Azure.WAF/pillar' = 'Reliability'; } -Labels @{ 'Azure.WAF/maturity' = 'L1' } {
# Check for availability zones based on virtual machine scale sets, because it is not exposed through the provider for Event Hub.
$provider = [PSRule.Rules.Azure.Runtime.Helper]::GetResourceType('Microsoft.Compute', 'virtualMachineScaleSets');
$availabilityZones = GetAvailabilityZone -Location $TargetObject.Location -Zone $provider.ZoneMappings;

# Don't flag if the region does not support AZ.
if (-not $availabilityZones) {
return $Assert.Pass();
}

$Assert.HasFieldValue($TargetObject, 'properties.zoneRedundant', $true);
}

#endregion Rules

#region Helper functions
Expand Down
44 changes: 31 additions & 13 deletions tests/PSRule.Rules.Azure.Tests/Azure.EventHub.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ Describe 'Azure.EventHub' -Tag 'EventHub' {
# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 5;
$ruleResult.TargetName | Should -BeIn 'hubns-B', 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F';
$ruleResult.Length | Should -Be 7;
$ruleResult.TargetName | Should -BeIn 'hubns-B', 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F', 'hubns-G', 'hubns-H';

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
Expand All @@ -58,8 +58,8 @@ Describe 'Azure.EventHub' -Tag 'EventHub' {
# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 5;
$ruleResult.TargetName | Should -BeIn 'hubns-B', 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F';
$ruleResult.Length | Should -Be 7;
$ruleResult.TargetName | Should -BeIn 'hubns-B', 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F', 'hubns-G', 'hubns-H';

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
Expand All @@ -82,8 +82,8 @@ Describe 'Azure.EventHub' -Tag 'EventHub' {
# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 4;
$ruleResult.TargetName | Should -BeIn 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F';
$ruleResult.Length | Should -Be 6;
$ruleResult.TargetName | Should -BeIn 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F', 'hubns-G', 'hubns-H';
}

It 'Azure.EventHub.Firewall' {
Expand All @@ -92,17 +92,35 @@ Describe 'Azure.EventHub' -Tag 'EventHub' {
# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult.Length | Should -Be 4;
$ruleResult.TargetName | Should -BeIn 'hubns-B', 'hubns-C', 'hubns-D', 'default-A';
$ruleResult.TargetName | Should -BeIn 'hubns-B', 'hubns-C', 'hubns-D', 'hubns-X/default-A';

$ruleResult[0].Reason | Should -BeExactly "Path properties.publicNetworkAccess: Does not exist.";
$ruleResult[1].Reason | Should -BeIn "Path properties.publicNetworkAccess: Is set to 'Enabled'.";
$ruleResult[2..3].Reason | Should -BeIn "Path properties.publicNetworkAccess: Is set to 'Enabled'.", "Path properties.defaultAction: Is set to 'Allow'."

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult.Length | Should -Be 7;
$ruleResult.TargetName | Should -BeIn 'hubns-E', 'hubns-F', 'hubns-Y/default-B', 'default-C', 'default-D', 'hubns-G', 'hubns-H';
}

It 'Azure.EventHub.AvailabilityZone' {
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.EventHub.AvailabilityZone' };

$ruleResult[0].Reason | Should -BeExactly "Path properties.publicNetworkAccess: Does not exist."
$ruleResult[1].Reason | Should -BeExactly "Path properties.publicNetworkAccess: Is set to 'Enabled'."
$ruleResult[2].Reason | Should -BeIn "Path properties.publicNetworkAccess: Is set to 'Enabled'.", "Path properties.defaultAction: Is set to 'Allow'."
$ruleResult[3].Reason | Should -BeIn "Path properties.publicNetworkAccess: Is set to 'Enabled'.", "Path properties.defaultAction: Is set to 'Allow'."
# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 2;
$ruleResult.TargetName | Should -BeIn 'hubns-A', 'hubns-B';

$ruleResult[0].Reason | Should -BeExactly "Path properties.zoneRedundant: Does not exist.";
$ruleResult[1].Reason | Should -BeExactly "Path properties.zoneRedundant: Is set to 'False'.";

Comment on lines 116 to 118
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot one down

Suggested change
$ruleResult[0].Reason | Should -BeExactly "Path properties.zoneRedundant: Does not exist.";
$ruleResult[1].Reason | Should -BeExactly "Path properties.zoneRedundant: Is set to 'False'.";
$ruleResult[0].Reason | Should -BeExactly "Path properties.zoneRedundant: Does not exist.";
$ruleResult[1].Reason | Should -BeExactly "Path properties.zoneRedundant: Is set to 'False'.";

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in commit 600ac56.

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult.Length | Should -Be 5;
$ruleResult.TargetName | Should -BeIn 'hubns-E', 'hubns-F', 'default-B', 'default-C', 'default-D';
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 6;
$ruleResult.TargetName | Should -BeIn 'hubns-C', 'hubns-D', 'hubns-E', 'hubns-F', 'hubns-G', 'hubns-H';
}
}

Expand Down
Loading
Loading