fix(azure): tighten flow log workspace checks (#10645)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Davlet Dzhakishev
2026-04-28 16:57:04 +02:00
committed by GitHub
parent 13d983450c
commit 1de01bcb78
7 changed files with 266 additions and 36 deletions
+4
View File
@@ -8,6 +8,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `bedrock_guardrails_configured` check for AWS provider [(#10844)](https://github.com/prowler-cloud/prowler/pull/10844)
### 🔄 Changed
- Azure Network Watcher flow log checks now require workspace-backed Traffic Analytics for `network_flow_log_captured_sent` and align metadata with VNet-compatible flow log guidance [(#10645)](https://github.com/prowler-cloud/prowler/pull/10645)
---
## [5.25.0] (Prowler v5.25.0)
@@ -9,8 +9,8 @@
"Severity": "high",
"ResourceType": "microsoft.network/networkwatchers",
"ResourceGroup": "network",
"Description": "**Azure Network Watcher** has **NSG flow logs** enabled and configured to forward traffic records to a centralized **Log Analytics workspace**",
"Risk": "Missing or disabled flow logging blinds visibility into network behavior, hindering detection of:\n- **Lateral movement** and internal scanning\n- **C2 beacons** and exfiltration patterns\nThis degrades incident response and correlation, impacting **confidentiality** and **integrity**.",
"Description": "**Azure Network Watcher** has **flow logs** enabled for supported targets, such as **virtual networks** and **network security groups**, and configured with **Traffic Analytics** to forward records to a centralized **Log Analytics workspace**",
"Risk": "Missing, disabled, or non-centralized flow logging blinds visibility into network behavior, hindering detection of:\n- **Lateral movement** and internal scanning\n- **C2 beacons** and exfiltration patterns\nThis degrades incident response and correlation, impacting **confidentiality** and **integrity**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-tutorial",
@@ -18,13 +18,13 @@
],
"Remediation": {
"Code": {
"CLI": "az network watcher flow-log create --location <REGION> --name <FLOW_LOG_NAME> --resource-group <RESOURCE_GROUP> --nsg <NSG_NAME> --storage-account <STORAGE_ACCOUNT_NAME> --enabled true --workspace <LOG_ANALYTICS_WORKSPACE_ID>",
"NativeIaC": "```bicep\n// Enable NSG flow logs and send to Log Analytics\nresource flowLog 'Microsoft.Network/networkWatchers/flowLogs@2022-09-01' = {\n name: '<example_resource_name>/<example_resource_name>'\n location: '<REGION>'\n properties: {\n enabled: true // CRITICAL: turns on flow logs\n targetResourceId: '<example_resource_id>' // NSG resource ID\n storageId: '<example_resource_id>' // required for NSG flow logs\n flowAnalyticsConfiguration: {\n networkWatcherFlowAnalyticsConfiguration: {\n enabled: true // CRITICAL: sends flow logs to Log Analytics\n workspaceResourceId: '<example_resource_id>' // Log Analytics workspace resource ID\n }\n }\n }\n}\n```",
"Other": "1. In Azure portal, go to Network Watcher > Flow logs\n2. Click + Create (or Create flow log)\n3. Select the target NSG and region\n4. Set Status to On\n5. Select a Storage account\n6. Enable Traffic analytics, then select your Log Analytics workspace\n7. Click Review + create, then Create",
"Terraform": "```hcl\n# Enable NSG flow logs and send to Log Analytics\nresource \"azurerm_network_watcher_flow_log\" \"<example_resource_name>\" {\n network_watcher_name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n network_security_group_id = \"<example_resource_id>\"\n storage_account_id = \"<example_resource_id>\"\n\n enabled = true # CRITICAL: turns on flow logs\n\n traffic_analytics { \n enabled = true # CRITICAL: sends flow logs to Log Analytics\n workspace_id = \"<example_resource_id>\" # workspace_id (GUID) or use data source\n workspace_region = \"<REGION>\"\n workspace_resource_id = \"<example_resource_id>\" # Log Analytics workspace resource ID\n }\n}\n```"
"CLI": "az network watcher flow-log create --location <REGION> --name <FLOW_LOG_NAME> --resource-group <RESOURCE_GROUP> --target-resource-id <TARGET_RESOURCE_ID> --storage-account <STORAGE_ACCOUNT_ID> --enabled true --workspace <LOG_ANALYTICS_WORKSPACE_ID>",
"NativeIaC": "```bicep\n// Enable flow logs for a supported target (for example, a virtual network or NSG)\nresource flowLog 'Microsoft.Network/networkWatchers/flowLogs@2023-09-01' = {\n name: '<example_network_watcher_name>/<example_flow_log_name>'\n location: '<REGION>'\n properties: {\n enabled: true\n targetResourceId: '<example_target_resource_id>'\n storageId: '<example_storage_account_id>'\n flowAnalyticsConfiguration: {\n networkWatcherFlowAnalyticsConfiguration: {\n enabled: true\n workspaceResourceId: '<example_log_analytics_workspace_id>'\n }\n }\n }\n}\n```",
"Other": "1. In Azure portal, go to Network Watcher > Flow logs\n2. Click + Create\n3. Select the subscription and region\n4. Choose the appropriate flow log type and target resource, such as a virtual network or network security group\n5. Set Status to On\n6. Select a Storage account\n7. Enable Traffic analytics and select the Log Analytics workspace\n8. Click Review + create, then Create",
"Terraform": "```hcl\n# Enable flow logs for a supported target and send analytics to Log Analytics\nresource \"azurerm_network_watcher_flow_log\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n network_watcher_name = \"<example_network_watcher_name>\"\n resource_group_name = \"<example_resource_group_name>\"\n target_resource_id = \"<example_target_resource_id>\"\n storage_account_id = \"<example_storage_account_id>\"\n\n enabled = true\n\n traffic_analytics {\n enabled = true\n workspace_id = \"<example_workspace_id>\"\n workspace_region = \"<REGION>\"\n workspace_resource_id = \"<example_log_analytics_workspace_id>\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable and centrally aggregate **NSG flow logs** to a **Log Analytics workspace**.\n\n- Enforce least privilege on log data\n- Define retention and secure storage\n- Use layered monitoring (e.g., Traffic Analytics)\n- Ensure coverage across regions/subscriptions and critical NSGs",
"Text": "Enable and centrally aggregate **flow logs** for supported Network Watcher targets, including **virtual networks** and **network security groups**, to a **Log Analytics workspace**.\n\n- Enforce least privilege on log data\n- Define retention and secure storage\n- Use layered monitoring (e.g., Traffic Analytics)\n- Ensure coverage across regions, subscriptions, and critical network segments",
"Url": "https://hub.prowler.com/check/network_flow_log_captured_sent"
}
},
@@ -34,5 +34,5 @@
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "The impact of configuring NSG Flow logs is primarily one of cost and configuration. If deployed, it will create storage accounts that hold minimal amounts of data on a 5-day lifecycle before feeding to Log Analytics Workspace. This will increase the amount of data stored and used by Azure Monitor."
"Notes": "Configuring flow logs and Traffic Analytics increases storage and analytics costs. For new Azure deployments, prefer virtual network flow logs where they satisfy your monitoring requirements because NSG flow logs are on the retirement path."
}
@@ -11,16 +11,26 @@ class network_flow_log_captured_sent(Check):
metadata=self.metadata(), resource=network_watcher
)
report.subscription = subscription
report.status = "FAIL"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has no flow logs"
if network_watcher.flow_logs:
report.status = "FAIL"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs disabled"
report.status = "PASS"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs that are captured and sent to Log Analytics workspace"
has_failed = False
for flow_log in network_watcher.flow_logs:
if flow_log.enabled:
report.status = "PASS"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs that are captured and sent to Log Analytics workspace"
break
if not has_failed:
if not flow_log.enabled:
report.status = "FAIL"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has flow logs disabled"
has_failed = True
elif not (
flow_log.traffic_analytics_enabled
and flow_log.workspace_resource_id
):
report.status = "FAIL"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace"
has_failed = True
else:
report.status = "FAIL"
report.status_extended = f"Network Watcher {network_watcher.name} from subscription {subscription} has no flow logs"
findings.append(report)
@@ -9,8 +9,8 @@
"Severity": "medium",
"ResourceType": "microsoft.network/networkwatchers",
"ResourceGroup": "network",
"Description": "**Azure Network Watcher** has **NSG flow logs** enabled and configured to retain for at least `90` days (or `0` for unlimited). The evaluation checks that flow logging is enabled and that the retention policy meets the required duration for each configured log.",
"Risk": "Absent or short-retained **NSG flow logs** reduce visibility into IP flows, delaying detection of port scans, brute force, data exfiltration, and lateral movement.\n\nForensics and accountability degrade, threatening **confidentiality** and **integrity**.",
"Description": "**Azure Network Watcher** has **flow logs** enabled for supported targets, such as **virtual networks** and **network security groups**, and configured to retain for at least `90` days (or `0` for unlimited). The evaluation checks that flow logging is enabled and that the retention policy meets the required duration for each configured log.",
"Risk": "Absent or short-retained **flow logs** reduce visibility into IP flows, delaying detection of port scans, brute force, data exfiltration, and lateral movement.\n\nForensics and accountability degrade, threatening **confidentiality** and **integrity**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/cli/azure/network/watcher/flow-log?view=azure-cli-latest",
@@ -20,13 +20,13 @@
],
"Remediation": {
"Code": {
"CLI": "az network watcher flow-log create --location <LOCATION> --name <example_resource_name> --nsg <example_resource_id> --storage-account <example_resource_id> --retention 90",
"NativeIaC": "```bicep\n// Enable NSG flow logs with retention >= 90 days\nresource flowlog 'Microsoft.Network/networkWatchers/flowLogs@2023-09-01' = {\n name: '<example_resource_name>/<example_resource_name>'\n location: '<LOCATION>'\n properties: {\n targetResourceId: '<example_resource_id>'\n storageId: '<example_resource_id>'\n enabled: true // critical: turns on flow logs\n retentionPolicy: {\n enabled: true // critical: activates retention policy\n days: 90 // critical: 0 (unlimited) or >= 90 to pass\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Network Watcher > NSG flow logs\n2. Select the NSG to configure\n3. Set Status to On\n4. Set Retention (days) to 0 (unlimited) or at least 90\n5. Select a Storage account\n6. Click Save",
"Terraform": "```hcl\n# Enable NSG flow logs with retention >= 90 days\nresource \"azurerm_network_watcher_flow_log\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n network_watcher_name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n target_resource_id = \"<example_resource_id>\"\n storage_account_id = \"<example_resource_id>\"\n\n enabled = true # critical: turns on flow logs\n\n retention_policy {\n enabled = true # critical: activates retention policy\n days = 90 # critical: 0 (unlimited) or >= 90 to pass\n }\n}\n```"
"CLI": "az network watcher flow-log create --location <LOCATION> --name <example_flow_log_name> --target-resource-id <example_target_resource_id> --storage-account <example_storage_account_id> --enabled true --retention 90",
"NativeIaC": "```bicep\n// Enable flow logs with retention >= 90 days for a supported target\nresource flowlog 'Microsoft.Network/networkWatchers/flowLogs@2023-09-01' = {\n name: '<example_network_watcher_name>/<example_flow_log_name>'\n location: '<LOCATION>'\n properties: {\n targetResourceId: '<example_target_resource_id>'\n storageId: '<example_storage_account_id>'\n enabled: true\n retentionPolicy: {\n enabled: true\n days: 90\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Network Watcher > Flow logs\n2. Select the relevant flow log or create one for the target resource, such as a virtual network or network security group\n3. Set Status to On\n4. Set Retention (days) to 0 (unlimited) or at least 90\n5. Select a Storage account\n6. Click Save or Review + create",
"Terraform": "```hcl\n# Enable flow logs with retention >= 90 days\nresource \"azurerm_network_watcher_flow_log\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n network_watcher_name = \"<example_network_watcher_name>\"\n resource_group_name = \"<example_resource_group_name>\"\n target_resource_id = \"<example_target_resource_id>\"\n storage_account_id = \"<example_storage_account_id>\"\n\n enabled = true\n\n retention_policy {\n enabled = true\n days = 90\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable **NSG flow logs** and keep retention `90` days (`0` for unlimited). Restrict and monitor access to logs, store immutably, and stream to a SIEM to detect anomalies. Apply **defense in depth** and **least privilege**. Plan migration to **Virtual network flow logs** as NSG flow logs are being retired.",
"Text": "Enable **flow logs** and keep retention `90` days (`0` for unlimited) for supported targets, including **virtual networks** and **network security groups**. Restrict and monitor access to logs, store immutably, and stream to a SIEM to detect anomalies. Apply **defense in depth** and **least privilege**. Prefer **virtual network flow logs** for new deployments as NSG flow logs are being retired.",
"Url": "https://hub.prowler.com/check/network_flow_log_more_than_90_days"
}
},
@@ -36,5 +36,5 @@
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This will keep IP traffic logs for longer than 90 days. As a level 2, first determine your need to retain data, then apply your selection here. As this is data stored for longer, your monthly storage costs will increase depending on your data use."
"Notes": "Longer retention improves investigation depth but increases storage cost. For new Azure deployments, prefer virtual network flow logs where they satisfy your monitoring requirements because NSG flow logs are on the retirement path."
}
@@ -79,6 +79,9 @@ class Network(AzureService):
id=flow_log.id,
name=flow_log.name,
enabled=flow_log.enabled,
target_resource_id=getattr(
flow_log, "target_resource_id", None
),
retention_policy=RetentionPolicy(
enabled=(
flow_log.retention_policy.enabled
@@ -91,6 +94,34 @@ class Network(AzureService):
else 0
),
),
traffic_analytics_enabled=bool(
getattr(
getattr(
getattr(
flow_log,
"flow_analytics_configuration",
None,
),
"network_watcher_flow_analytics_configuration",
None,
),
"enabled",
False,
)
),
workspace_resource_id=getattr(
getattr(
getattr(
flow_log,
"flow_analytics_configuration",
None,
),
"network_watcher_flow_analytics_configuration",
None,
),
"workspace_resource_id",
None,
),
)
for flow_log in flow_logs
],
@@ -192,6 +223,9 @@ class FlowLog:
name: str
enabled: bool
retention_policy: RetentionPolicy
target_resource_id: Optional[str] = None
traffic_analytics_enabled: bool = False
workspace_resource_id: Optional[str] = None
@dataclass
@@ -1,9 +1,11 @@
from unittest import mock
from uuid import uuid4
from azure.mgmt.network.models import FlowLog, RetentionPolicyParameters
from prowler.providers.azure.services.network.network_service import NetworkWatcher
from prowler.providers.azure.services.network.network_service import (
FlowLog,
NetworkWatcher,
RetentionPolicy,
)
from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION_ID
@@ -86,8 +88,11 @@ class Test_network_flow_log_captured_sent:
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="disabled-flow-log",
enabled=False,
retention_policy=RetentionPolicyParameters(days=90),
target_resource_id=None,
retention_policy=RetentionPolicy(days=90),
)
],
)
@@ -134,8 +139,171 @@ class Test_network_flow_log_captured_sent:
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="workspace-disabled",
enabled=True,
retention_policy=RetentionPolicyParameters(days=90),
target_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/test-vnet",
retention_policy=RetentionPolicy(days=90),
)
],
)
]
}
with (
mock.patch(
"prowler.providers.azure.services.network.network_service.Network",
new=network_client,
) as service_client,
mock.patch(
"prowler.providers.azure.services.network.network_client.network_client",
new=service_client,
),
):
from prowler.providers.azure.services.network.network_flow_log_captured_sent.network_flow_log_captured_sent import (
network_flow_log_captured_sent,
)
check = network_flow_log_captured_sent()
result = check.execute()
assert len(result) == 1
assert result[0].location == "location"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == network_watcher_name
assert result[0].resource_id == network_watcher_id
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace"
)
def test_network_network_watchers_traffic_analytics_without_workspace(self):
network_client = mock.MagicMock
network_watcher_name = "Network Watcher Name"
network_watcher_id = str(uuid4())
network_client.network_watchers = {
AZURE_SUBSCRIPTION_ID: [
NetworkWatcher(
id=network_watcher_id,
name=network_watcher_name,
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="ta-without-workspace",
enabled=True,
target_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/test-vnet",
retention_policy=RetentionPolicy(days=90),
traffic_analytics_enabled=True,
workspace_resource_id=None,
)
],
)
]
}
with (
mock.patch(
"prowler.providers.azure.services.network.network_service.Network",
new=network_client,
) as service_client,
mock.patch(
"prowler.providers.azure.services.network.network_client.network_client",
new=service_client,
),
):
from prowler.providers.azure.services.network.network_flow_log_captured_sent.network_flow_log_captured_sent import (
network_flow_log_captured_sent,
)
check = network_flow_log_captured_sent()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace"
)
def test_network_network_watchers_mixed_flow_logs_fails(self):
network_client = mock.MagicMock
network_watcher_name = "Network Watcher Name"
network_watcher_id = str(uuid4())
network_client.network_watchers = {
AZURE_SUBSCRIPTION_ID: [
NetworkWatcher(
id=network_watcher_id,
name=network_watcher_name,
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="vnet-flow-log-workspace-backed",
enabled=True,
target_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/test-vnet",
retention_policy=RetentionPolicy(days=90),
traffic_analytics_enabled=True,
workspace_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/test-law",
),
FlowLog(
id=str(uuid4()),
name="nsg-flow-log-storage-only",
enabled=True,
target_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg",
retention_policy=RetentionPolicy(days=90),
traffic_analytics_enabled=False,
workspace_resource_id=None,
),
],
)
]
}
with (
mock.patch(
"prowler.providers.azure.services.network.network_service.Network",
new=network_client,
) as service_client,
mock.patch(
"prowler.providers.azure.services.network.network_client.network_client",
new=service_client,
),
):
from prowler.providers.azure.services.network.network_flow_log_captured_sent.network_flow_log_captured_sent import (
network_flow_log_captured_sent,
)
check = network_flow_log_captured_sent()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Network Watcher {network_watcher_name} from subscription {AZURE_SUBSCRIPTION_ID} has enabled flow logs that are not configured to send traffic analytics to a Log Analytics workspace"
)
def test_network_network_watchers_vnet_flow_logs_well_configured(self):
network_client = mock.MagicMock
network_watcher_name = "Network Watcher Name"
network_watcher_id = str(uuid4())
network_client.network_watchers = {
AZURE_SUBSCRIPTION_ID: [
NetworkWatcher(
id=network_watcher_id,
name=network_watcher_name,
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="vnet-flow-log",
enabled=True,
target_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/test-vnet",
retention_policy=RetentionPolicy(days=90),
traffic_analytics_enabled=True,
workspace_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/test-law",
)
],
)
@@ -1,9 +1,11 @@
from unittest import mock
from uuid import uuid4
from azure.mgmt.network.models import FlowLog, RetentionPolicyParameters
from prowler.providers.azure.services.network.network_service import NetworkWatcher
from prowler.providers.azure.services.network.network_service import (
FlowLog,
NetworkWatcher,
RetentionPolicy,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
@@ -97,8 +99,11 @@ class Test_network_flow_log_more_than_90_days:
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="disabled-flow-log",
enabled=False,
retention_policy=RetentionPolicyParameters(days=90),
target_resource_id=None,
retention_policy=RetentionPolicy(days=90),
)
],
)
@@ -149,8 +154,11 @@ class Test_network_flow_log_more_than_90_days:
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="retention-80",
enabled=True,
retention_policy=RetentionPolicyParameters(days=80),
target_resource_id=None,
retention_policy=RetentionPolicy(days=80),
)
],
)
@@ -201,8 +209,11 @@ class Test_network_flow_log_more_than_90_days:
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="retention-0",
enabled=True,
retention_policy=RetentionPolicyParameters(days=0),
target_resource_id=None,
retention_policy=RetentionPolicy(days=0),
)
],
)
@@ -253,8 +264,11 @@ class Test_network_flow_log_more_than_90_days:
location="location",
flow_logs=[
FlowLog(
id=str(uuid4()),
name="vnet-flow-log",
enabled=True,
retention_policy=RetentionPolicyParameters(days=90),
target_resource_id="/subscriptions/test-sub/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/test-vnet",
retention_policy=RetentionPolicy(days=90),
)
],
)