diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index b1e11b6..6c36735 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -172,6 +172,11 @@ "control_id": "6.4", "control_name": "Ensure that Azure Firewall is enabled on Virtual Networks", "description": "VNet peering connections with allowGatewayTransit or useRemoteGateways enabled allow traffic to route between network segments through shared gateways. This can break network segmentation and enable lateral movement between zones that should remain isolated. Peering connections should be reviewed and gateway transit disabled unless explicitly required and documented." + }, + "AZ-NET-015": { + "control_id": "9.2", + "control_name": "Ensure that Network Security Groups have rules configured", + "description": "Public DNS zones expose all DNS records to internet enumeration, allowing attackers to map hostnames, IP addresses, and service endpoints. CIS 9.2 requires that network access controls are explicitly configured. Internal services should use Azure Private DNS zones linked to virtual networks rather than public zones." } } } diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index f9e3f97..6cd8943 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -172,6 +172,12 @@ "control_id": "A.13.1.1", "control_name": "Network controls", "description": "VNet peering connections with gateway transit enabled allow traffic to flow between network segments through shared gateways, potentially bypassing network controls. Networks should be managed and controlled to protect information in systems and applications. Gateway transit on peering connections should be disabled unless explicitly required." + }, + "AZ-NET-015": { + "control_id": "A.13.1.1", + "control_name": "Network controls", + "description": "Public DNS zones expose internal hostnames, IP addresses, and service endpoints to enumeration by any internet user. Networks should be managed and controlled to protect information systems. Internal services should use Azure Private DNS zones to restrict resolution to authorised networks." } } } + diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 30ae4b5..05f255d 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -172,6 +172,12 @@ "control_id": "PR.AC-5", "control_name": "Network integrity is protected", "description": "VNet peering with gateway transit enabled allows traffic to cross network boundaries through shared gateways, undermining network segmentation. PR.AC-5 requires that network integrity is protected. Disabling gateway transit on peering connections enforces boundary integrity between network zones." + }, + "AZ-NET-015": { + "control_id": "PR.AC-5", + "control_name": "Network integrity is protected", + "description": "Public DNS zones allow unrestricted internet enumeration of DNS records, exposing internal hostnames and IP addresses. PR.AC-5 requires that network integrity is protected. Internal services should be resolved through Azure Private DNS zones linked to trusted virtual networks." } } } + diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index 3bf94d0..4477828 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -167,6 +167,11 @@ "control_id": "CC6.6", "control_name": "Restricts Access from Outside the Network Boundary", "description": "VNet peering with allowGatewayTransit or useRemoteGateways enabled allows traffic to cross network boundaries through shared gateways, weakening the logical separation between network zones. CC6.6 requires that logical access from outside the network boundary is restricted and controlled. Gateway transit on peering connections should be disabled to enforce boundary separation." + }, + "AZ-NET-015": { + "control_id": "CC6.6", + "control_name": "Restricts Access from Outside the Network Boundary", + "description": "A public DNS zone allows any external party to query and enumerate DNS records, exposing internal hostnames and service endpoints. CC6.6 requires that logical access from outside the network boundary is restricted and controlled. Internal services should use Azure Private DNS zones linked to virtual networks." } - } -} + } +} diff --git a/playbooks/cli/fix_az_net_015.sh b/playbooks/cli/fix_az_net_015.sh new file mode 100644 index 0000000..77814d0 --- /dev/null +++ b/playbooks/cli/fix_az_net_015.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +RESOURCE_GROUP=$1 +ZONE_NAME=$2 +VNET_ID=$3 + +if [ -z "$RESOURCE_GROUP" ] || [ -z "$ZONE_NAME" ] || [ -z "$VNET_ID" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Creating private DNS zone: $ZONE_NAME" + +az network private-dns zone create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$ZONE_NAME" + +az network private-dns link vnet create \ + --resource-group "$RESOURCE_GROUP" \ + --zone-name "$ZONE_NAME" \ + --name "${ZONE_NAME}-link" \ + --virtual-network "$VNET_ID" \ + --registration-enabled false + +echo "Done. Private DNS zone created and linked to VNet." +echo "Note: Migrate records from the public zone to the private zone before deleting the public zone." \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 43d9ede..da2122a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ azure-mgmt-rdbms==10.1.0 azure-mgmt-authorization==4.0.0 azure-monitor-ingestion==1.0.3 azure-mgmt-monitor==6.0.0 +azure-mgmt-dns==8.0.0 psycopg2-binary==2.9.9 python-dotenv==1.0.0 pyjwt==2.8.0 diff --git a/scanner/azure_client.py b/scanner/azure_client.py index ef331ea..e507a8f 100644 --- a/scanner/azure_client.py +++ b/scanner/azure_client.py @@ -284,6 +284,16 @@ def get_vnet_peerings(self, resource_group: str, vnet_name: str) -> List[Any]: except Exception as exc: logger.error("get_vnet_peerings(%s) failed: %s", vnet_name, exc) return [] + def get_dns_zones(self) -> List[Any]: + """List all DNS zones in the subscription.""" + try: + from azure.mgmt.dns import DnsManagementClient + client = DnsManagementClient(self.credential, self.subscription_id) + return list(client.zones.list()) + except Exception as exc: + logger.error("get_dns_zones failed: %s", exc) + return [] + # ------------------------------------------------------------------ # # Compute # diff --git a/scanner/rules/az_net_015.py b/scanner/rules/az_net_015.py new file mode 100644 index 0000000..a337ba5 --- /dev/null +++ b/scanner/rules/az_net_015.py @@ -0,0 +1,54 @@ +"""AZ-NET-015: Public DNS zone exposes internal infrastructure to enumeration.""" +from typing import Any, Dict, List + +RULE_ID = "AZ-NET-015" +RULE_NAME = "Public DNS Zone Exposes Infrastructure to Enumeration" +SEVERITY = "MEDIUM" +CATEGORY = "Network" +FRAMEWORKS = { + "CIS": "9.2", + "NIST": "PR.AC-5", + "ISO27001": "A.13.1.1", + "SOC2": "CC6.6" +} +DESCRIPTION = ( + "The DNS zone is configured as a Public zone, meaning its records " + "are queryable by anyone on the internet. Public DNS zones expose " + "internal hostnames, IP addresses, and service endpoints to " + "enumeration, which can assist attackers in mapping the organisation's " + "infrastructure and identifying targets for further attack." +) +REMEDIATION = ( + "Review all DNS zones and migrate records for internal services to " + "Azure Private DNS zones linked to the appropriate virtual networks. " + "Retain public DNS zones only for resources that are intentionally " + "internet-facing and require public resolution." +) +PLAYBOOK = "playbooks/cli/fix_az_net_015.sh" + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + findings: List[Dict[str, Any]] = [] + for zone in azure_client.get_dns_zones(): + parsed = azure_client.parse_resource_id(zone.id) + resource_group = parsed["resource_group"] + zone_type = getattr(zone, "zone_type", "Public") + if zone_type == "Public": + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": zone.id, + "resource_name": zone.name, + "resource_type": "Microsoft.Network/dnsZones", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": resource_group, + "zone_type": zone_type + } + }) + return findings \ No newline at end of file