mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(cloudflare): plan-aware WAF FAIL hints for zones (#9896)
This commit is contained in:
committed by
GitHub
parent
78af0c24fe
commit
6befa78978
@@ -19,6 +19,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `zone_waf_enabled` check for Cloudflare provider now appends a plan-aware hint to the FAIL `status_extended`: a possible-false-positive note on paid plans (Pro, Business, Enterprise) where the legacy `waf` zone setting can read `off` even though WAF managed rulesets are deployed via the dashboard, and a "not available on the Cloudflare Free plan" note on Free zones [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896)
|
||||
- Google Workspace Gmail checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11169)](https://github.com/prowler-cloud/prowler/pull/11169)
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import Optional
|
||||
|
||||
# Cloudflare returns the plan name in ``zone.plan.name`` (e.g. "Free Website",
|
||||
# "Pro Website", "Business Website", "Enterprise Website"). Free plans do not
|
||||
# expose WAF managed rulesets at all, while paid plans expose them but the
|
||||
# legacy ``waf`` zone setting can lag behind the actual deployment state.
|
||||
PAID_PLAN_KEYWORDS = ("pro", "business", "enterprise")
|
||||
FREE_PLAN_KEYWORDS = ("free",)
|
||||
|
||||
|
||||
def _plan_matches(plan: Optional[str], keywords: tuple[str, ...]) -> bool:
|
||||
if not isinstance(plan, str):
|
||||
return False
|
||||
plan_lower = plan.lower()
|
||||
return any(keyword in plan_lower for keyword in keywords)
|
||||
|
||||
|
||||
def is_paid_plan(plan: Optional[str]) -> bool:
|
||||
"""Return True when the Cloudflare zone plan is a paid tier."""
|
||||
return _plan_matches(plan, PAID_PLAN_KEYWORDS)
|
||||
|
||||
|
||||
def is_free_plan(plan: Optional[str]) -> bool:
|
||||
"""Return True when the Cloudflare zone plan is the Free tier."""
|
||||
return _plan_matches(plan, FREE_PLAN_KEYWORDS)
|
||||
|
||||
|
||||
def paid_plan_suffix(plan: Optional[str], message: str) -> str:
|
||||
"""Return an explanatory suffix only when the zone is on a paid plan."""
|
||||
return f" {message}" if is_paid_plan(plan) else ""
|
||||
|
||||
|
||||
def free_plan_suffix(plan: Optional[str], message: str) -> str:
|
||||
"""Return an explanatory suffix only when the zone is on the Free plan."""
|
||||
return f" {message}" if is_free_plan(plan) else ""
|
||||
@@ -1,6 +1,19 @@
|
||||
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||
from prowler.providers.cloudflare.lib.plan import (
|
||||
free_plan_suffix,
|
||||
paid_plan_suffix,
|
||||
)
|
||||
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||
|
||||
PAID_PLAN_FALSE_POSITIVE_HINT = (
|
||||
"This may be a false positive if WAF managed rulesets are configured via "
|
||||
"the Cloudflare dashboard; verify manually in Security > WAF."
|
||||
)
|
||||
FREE_PLAN_UNAVAILABLE_HINT = (
|
||||
"This may be expected because the Web Application Firewall is not "
|
||||
"available on the Cloudflare Free plan."
|
||||
)
|
||||
|
||||
|
||||
class zone_waf_enabled(Check):
|
||||
"""Ensure that WAF is enabled for Cloudflare zones.
|
||||
@@ -35,6 +48,16 @@ class zone_waf_enabled(Check):
|
||||
report.status_extended = f"WAF is enabled for zone {zone.name}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"WAF is not enabled for zone {zone.name}."
|
||||
# Two plan-specific hints can be appended to the FAIL message:
|
||||
# - Paid plans: the legacy ``waf`` zone setting can read ``off``
|
||||
# while WAF managed rulesets are deployed via the dashboard,
|
||||
# so the FAIL may be a false positive.
|
||||
# - Free plans: WAF is not available at all, so the FAIL is
|
||||
# expected and the suffix points that out.
|
||||
report.status_extended = (
|
||||
f"WAF is not enabled for zone {zone.name}."
|
||||
f"{paid_plan_suffix(zone.plan, PAID_PLAN_FALSE_POSITIVE_HINT)}"
|
||||
f"{free_plan_suffix(zone.plan, FREE_PLAN_UNAVAILABLE_HINT)}"
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
@@ -136,3 +136,79 @@ class Test_zone_waf_enabled:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_zone_waf_disabled_paid_plan_includes_hint(self):
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
id=ZONE_ID,
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
plan="Pro Website",
|
||||
settings=CloudflareZoneSettings(
|
||||
waf="off",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_cloudflare_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled.zone_client",
|
||||
new=zone_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled import (
|
||||
zone_waf_enabled,
|
||||
)
|
||||
|
||||
check = zone_waf_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "WAF is not enabled" in result[0].status_extended
|
||||
assert "false positive" in result[0].status_extended
|
||||
assert "Cloudflare dashboard" in result[0].status_extended
|
||||
|
||||
def test_zone_waf_disabled_free_plan_includes_unavailable_hint(self):
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
id=ZONE_ID,
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
plan="Free Website",
|
||||
settings=CloudflareZoneSettings(
|
||||
waf="off",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_cloudflare_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled.zone_client",
|
||||
new=zone_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.cloudflare.services.zone.zone_waf_enabled.zone_waf_enabled import (
|
||||
zone_waf_enabled,
|
||||
)
|
||||
|
||||
check = zone_waf_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "WAF is not enabled" in result[0].status_extended
|
||||
assert "not available on the Cloudflare Free plan" in (
|
||||
result[0].status_extended
|
||||
)
|
||||
assert "false positive" not in result[0].status_extended
|
||||
|
||||
Reference in New Issue
Block a user