From 9cac32665a91aeee8a53cecc059afc9c58664a18 Mon Sep 17 00:00:00 2001 From: aav-wh <2d9c6kh58x@privaterelay.appleid.com> Date: Sat, 30 May 2026 05:52:51 +0100 Subject: [PATCH] feat: add AZ-NET-013 Azure Firewall not enabled on Virtual Network - Add scanner/rules/az_net_013.py to detect VNets without Azure Firewall - Add playbooks/cli/fix_az_net_013.sh for remediation - Add get_azure_firewalls() method to scanner/azure_client.py - Update CIS, ISO 27001, NIST CSF, SOC 2 compliance mappings Severity: HIGH | Category: Network Closes #91 --- .../frameworks/cis_azure_benchmark.json | 13 +++-- compliance/frameworks/iso27001.json | 14 ++--- compliance/frameworks/nist_csf.json | 7 ++- compliance/frameworks/soc2.json | 16 +++--- playbooks/cli/fix_az_net_013.sh | 33 ++++++++++++ scanner/azure_client.py | 12 ++++- scanner/rules/az_net_013.py | 53 +++++++++++++++++++ 7 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 playbooks/cli/fix_az_net_013.sh create mode 100644 scanner/rules/az_net_013.py diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index d0d636d..da3969a 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -107,11 +107,11 @@ "control_id": "8.3", "control_name": "Ensure that 'OS patching' is enabled for virtual machines", "description": "The virtual machine does not have automatic OS patching enabled. CIS 8.3 requires that OS patches are applied in a timely manner. Unpatched VMs are vulnerable to known exploits targeting unpatched OS vulnerabilities." - }, + }, "AZ-KV-001": { "control_id": "8.5", "control_name": "Ensure the Key Vault is Recoverable", - "description": "Azure Key Vault soft delete should be enabled on all Key Vaults. The soft delete feature allows recovery of deleted vaults and vault objects (keys, secrets, certificates) for a configurable retention period (7\u201390 days), protecting against accidental or malicious deletion." + "description": "Azure Key Vault soft delete should be enabled on all Key Vaults. The soft delete feature allows recovery of deleted vaults and vault objects (keys, secrets, certificates) for a configurable retention period (7–90 days), protecting against accidental or malicious deletion." }, "AZ-STOR-003": { "control_id": "3.7", @@ -151,7 +151,7 @@ "AZ-DB-004": { "control_id": "4.1.2", "control_name": "Ensure that 'Allow access to Azure services' for SQL Servers is disabled", - "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource \u2014 including services from other tenants \u2014 to connect to the server. This significantly increases the attack surface. Access should be restricted to specific trusted IP ranges or private endpoints." + "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource — including services from other tenants — to connect to the server. This significantly increases the attack surface. Access should be restricted to specific trusted IP ranges or private endpoints." }, "AZ-IDN-004": { "control_id": "1.14", @@ -162,6 +162,11 @@ "control_id": "8.5", "control_name": "Ensure that the expiration date is set on all certificates", "description": "A certificate stored in Azure Key Vault is expiring within 30 days and does not have auto-renewal configured. CIS 8.5 requires that expiration dates are monitored and certificates are renewed before expiry to prevent service outages and broken authentication flows." + }, + "AZ-NET-013": { + "control_id": "6.4", + "control_name": "Ensure that Azure Firewall is enabled on Virtual Networks", + "description": "Virtual Networks that do not have an Azure Firewall deployed rely solely on Network Security Groups for perimeter defence. NSGs provide no deep packet inspection, threat intelligence filtering, or centralised traffic logging. Azure Firewall should be deployed in a dedicated subnet to inspect and control all inbound and outbound traffic." } } -} +} \ No newline at end of file diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index 87647ff..ba5fc43 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -101,18 +101,13 @@ "AZ-CMP-003": { "control_id": "A.12.2.1", "control_name": "Controls against malware", - "description": "The virtual machine does not have a recognised endpoint protection extension installed. A.12.2.1 requires that detection, prevention and recovery controls are implemented to protect against malware." + "description": "The virtual machine does not have a recognised endpoint protection extension installed. A.12.2.1 requires that detection, prevention and recovery controls are implemented to protect against malware. Without endpoint protection, malware executing on the VM will not be detected or prevented." }, "AZ-CMP-004": { "control_id": "A.12.6.1", "control_name": "Management of technical vulnerabilities", "description": "The virtual machine does not have automatic OS patching enabled. A.12.6.1 requires that information about technical vulnerabilities is obtained and the organisation's exposure evaluated. Without automatic patching, known OS vulnerabilities remain unmitigated." }, - "AZ-CMP-003": { - "control_id": "A.12.2.1", - "control_name": "Controls against malware", - "description": "The virtual machine does not have a recognised endpoint protection extension installed. A.12.2.1 requires that detection, prevention and recovery controls are implemented to protect against malware. Without endpoint protection, malware executing on the VM will not be detected or prevented." - }, "AZ-KV-001": { "control_id": "A.17.2.1", "control_name": "Availability of information processing facilities", @@ -167,6 +162,11 @@ "control_id": "A.9.2.3", "control_name": "Management of privileged access rights", "description": "The allocation and use of privileged access rights should be restricted and controlled. PIM enforces just-in-time access with time limits and approval workflows, ensuring privileged access rights are tightly managed and not permanently assigned." + }, + "AZ-NET-013": { + "control_id": "A.13.1.1", + "control_name": "Network controls", + "description": "Virtual Networks without an Azure Firewall rely solely on Network Security Groups for perimeter defence. NSGs provide no deep packet inspection, threat intelligence filtering, or centralised traffic logging. Networks should be managed and controlled using a dedicated firewall to inspect and control all inbound and outbound traffic." } } -} +} \ No newline at end of file diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 365faae..4b73871 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -162,6 +162,11 @@ "control_id": "PR.AC-4", "control_name": "Access permissions and authorizations are managed", "description": "PIM ensures privileged access permissions are managed with time-bound activation and approval workflows. Without PIM, permanently assigned admin roles violate the principle of least privilege and increase the blast radius of compromised accounts." + }, + "AZ-NET-013": { + "control_id": "PR.AC-5", + "control_name": "Network integrity is protected", + "description": "Virtual Networks without an Azure Firewall rely solely on Network Security Groups for perimeter defence. NSGs provide no deep packet inspection, threat intelligence filtering, or centralised traffic logging. PR.AC-5 requires that network integrity is protected. Azure Firewall should be deployed to inspect and control all inbound and outbound traffic and enforce network integrity boundaries." } } -} +} \ No newline at end of file diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index 8fcc996..742a3e9 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -111,18 +111,13 @@ "AZ-CMP-003": { "control_id": "CC6.8", "control_name": "Prevents or Detects Unauthorized or Malicious Software", - "description": "The virtual machine does not have a recognised endpoint protection extension installed. CC6.8 requires that controls are implemented to prevent or detect and act upon the introduction of unauthorised or malicious software." + "description": "The virtual machine does not have a recognised endpoint protection extension installed. CC6.8 requires that controls are implemented to prevent or detect and act upon the introduction of unauthorized or malicious software. Without endpoint protection, malicious code executing on the VM will not be detected or blocked." }, "AZ-CMP-004": { "control_id": "CC7.1", "control_name": "System Vulnerabilities are Identified and Managed", "description": "The virtual machine does not have automatic OS patching enabled. CC7.1 requires that vulnerabilities in system components are identified and managed through a defined process. Without automatic patching, known OS vulnerabilities are left unmitigated and exploitable." }, - "AZ-CMP-003": { - "control_id": "CC6.8", - "control_name": "Prevents or Detects Unauthorized or Malicious Software", - "description": "The virtual machine does not have a recognised endpoint protection extension installed. CC6.8 requires that controls are implemented to prevent or detect and act upon the introduction of unauthorized or malicious software. Without endpoint protection, malicious code executing on the VM will not be detected or blocked." - }, "AZ-KV-001": { "control_id": "A1.2", "control_name": "Environmental Threats and Recovery", @@ -156,12 +151,17 @@ "AZ-DB-004": { "control_id": "CC6.6", "control_name": "Restricts Access from Outside the Network Boundary", - "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource \u2014 including services from other tenants \u2014 to connect to the database. CC6.6 requires that access from outside the network boundary is restricted to authorised sources. Disabling this setting and replacing it with explicit firewall rules or private endpoints enforces the network boundary and ensures only known and trusted systems can reach the SQL Server." + "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource — including services from other tenants — to connect to the database. CC6.6 requires that access from outside the network boundary is restricted to authorised sources. Disabling this setting and replacing it with explicit firewall rules or private endpoints enforces the network boundary and ensures only known and trusted systems can reach the SQL Server." }, "AZ-IDN-004": { "control_id": "CC6.3", "control_name": "Role-based access control", "description": "PIM provides role-based access control with time-bound activation for privileged roles. Without PIM, admin roles are permanently assigned with no controls, violating the requirement for managed and restricted privileged access." + }, + "AZ-NET-013": { + "control_id": "CC6.6", + "control_name": "Restricts Access from Outside the Network Boundary", + "description": "Virtual Networks without an Azure Firewall rely solely on Network Security Groups for perimeter defence. NSGs provide no deep packet inspection, threat intelligence filtering, or centralised traffic logging. CC6.6 requires that access from outside the network boundary is restricted and controlled through appropriate logical access controls. Azure Firewall should be deployed to inspect and control all inbound and outbound traffic." } } -} +} \ No newline at end of file diff --git a/playbooks/cli/fix_az_net_013.sh b/playbooks/cli/fix_az_net_013.sh new file mode 100644 index 0000000..060303c --- /dev/null +++ b/playbooks/cli/fix_az_net_013.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +RESOURCE_GROUP=$1 +VNET_NAME=$2 + +if [ -z "$RESOURCE_GROUP" ] || [ -z "$VNET_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Deploying Azure Firewall for VNet: $VNET_NAME in resource group: $RESOURCE_GROUP..." + +az network vnet subnet create \ + --resource-group "$RESOURCE_GROUP" \ + --vnet-name "$VNET_NAME" \ + --name AzureFirewallSubnet \ + --address-prefixes 10.0.1.0/26 + +az network public-ip create \ + --resource-group "$RESOURCE_GROUP" \ + --name "${VNET_NAME}-fw-pip" \ + --sku Standard \ + --allocation-method Static + +az network firewall create \ + --resource-group "$RESOURCE_GROUP" \ + --name "${VNET_NAME}-firewall" \ + --sku-name AZFW_VNet \ + --sku-tier Standard + +echo "Azure Firewall deployed for VNet: $VNET_NAME" +echo "Note: Configure network and application rules to control traffic before routing through the firewall." \ No newline at end of file diff --git a/scanner/azure_client.py b/scanner/azure_client.py index 4b932ce..7142cca 100644 --- a/scanner/azure_client.py +++ b/scanner/azure_client.py @@ -240,7 +240,6 @@ def get_virtual_networks(self) -> List[Any]: logger.error("get_virtual_networks failed: %s", exc) return [] - def get_public_ip_addresses(self) -> List[Any]: """List all public IP addresses in the subscription.""" try: @@ -250,6 +249,17 @@ def get_public_ip_addresses(self) -> List[Any]: logger.error("get_public_ip_addresses failed: %s", exc) return [] + def get_azure_firewalls(self, resource_group: str) -> List[Any]: + """List all Azure Firewalls in a resource group.""" + try: + client = NetworkManagementClient(self.credential, self.subscription_id) + return list(client.azure_firewalls.list(resource_group)) + except Exception as exc: + logger.error( + "get_azure_firewalls(%s) failed: %s", resource_group, exc + ) + return [] + # ------------------------------------------------------------------ # # Compute # # ------------------------------------------------------------------ # diff --git a/scanner/rules/az_net_013.py b/scanner/rules/az_net_013.py new file mode 100644 index 0000000..b750602 --- /dev/null +++ b/scanner/rules/az_net_013.py @@ -0,0 +1,53 @@ +"""AZ-NET-013: Azure Firewall not enabled on Virtual Network.""" +from typing import Any, Dict, List + +RULE_ID = "AZ-NET-013" +RULE_NAME = "Azure Firewall Not Enabled on Virtual Network" +SEVERITY = "HIGH" +CATEGORY = "Network" +FRAMEWORKS = { + "CIS": "6.4", + "NIST": "PR.AC-5", + "ISO27001": "A.13.1.1", + "SOC2": "CC6.6" +} +DESCRIPTION = ( + "The Virtual Network does not have an Azure Firewall deployed. " + "Without Azure Firewall, the VNet relies solely on Network Security " + "Groups for perimeter defence, which provides no deep packet " + "inspection, threat intelligence filtering, or centralised traffic " + "logging. This leaves the network vulnerable to lateral movement " + "and data exfiltration." +) +REMEDIATION = ( + "Deploy an Azure Firewall in a dedicated AzureFirewallSubnet within " + "the Virtual Network. Configure network and application rules to " + "control inbound and outbound traffic. Enable diagnostic logging to " + "a Log Analytics workspace." +) +PLAYBOOK = "playbooks/cli/fix_az_net_013.sh" + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + findings: List[Dict[str, Any]] = [] + for vnet in azure_client.get_virtual_networks(): + parsed = azure_client.parse_resource_id(vnet.id) + resource_group = parsed["resource_group"] + vnet_name = parsed["name"] + firewalls = azure_client.get_azure_firewalls(resource_group) + if not firewalls: + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": vnet.id, + "resource_name": vnet_name, + "resource_type": "Microsoft.Network/virtualNetworks", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": {"resource_group": resource_group} + }) + return findings \ No newline at end of file