mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
Compare commits
139 Commits
176c11b15b
...
PROWLER-82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef48c662ed | ||
|
|
395f101e05 | ||
|
|
d158e0673b | ||
|
|
4ab769aff3 | ||
|
|
8aa321c20d | ||
|
|
9818575395 | ||
|
|
00376582b2 | ||
|
|
ba40d18643 | ||
|
|
cc00ddf877 | ||
|
|
b29c688cdd | ||
|
|
ae69b4f80a | ||
|
|
8804ce1bd0 | ||
|
|
00c57cea8d | ||
|
|
1b2c08649e | ||
|
|
47317680e5 | ||
|
|
c7558a9f78 | ||
|
|
86ecec542b | ||
|
|
cb82a42035 | ||
|
|
2c69eb58c9 | ||
|
|
fcd9e2d40f | ||
|
|
f0c69874e0 | ||
|
|
21444f7880 | ||
|
|
3e3f56629f | ||
|
|
38f6ca9514 | ||
|
|
7f71b93eec | ||
|
|
12752a5839 | ||
|
|
eb76e2b986 | ||
|
|
42c56fa33a | ||
|
|
5dcdeed782 | ||
|
|
c35eaa8aa9 | ||
|
|
bca7c3a479 | ||
|
|
e03fb88ca2 | ||
|
|
cecf288d4f | ||
|
|
8c4d251c51 | ||
|
|
98d4e08cbb | ||
|
|
3c004582d7 | ||
|
|
726aeec64b | ||
|
|
3d1a0b1270 | ||
|
|
b014fdbde3 | ||
|
|
d693a34747 | ||
|
|
a6860ffa7d | ||
|
|
d06af16a5c | ||
|
|
0250bc3b0e | ||
|
|
ee1e6c35f2 | ||
|
|
2e552c65a5 | ||
|
|
910514e964 | ||
|
|
695a3466cd | ||
|
|
7590ed7913 | ||
|
|
7fb82b0650 | ||
|
|
fa21a300fb | ||
|
|
d51fa60e58 | ||
|
|
9d69d3a25f | ||
|
|
74da022e48 | ||
|
|
da2b6d028b | ||
|
|
55d8a5d664 | ||
|
|
ecc1cf8b04 | ||
|
|
394a62fab1 | ||
|
|
0ef68f55a1 | ||
|
|
94b14d1592 | ||
|
|
36ac1bc47e | ||
|
|
41de65ceaa | ||
|
|
12f95e3a19 | ||
|
|
f5cada05c3 | ||
|
|
bfd8abdd89 | ||
|
|
99d2736116 | ||
|
|
b11e074f41 | ||
|
|
a4e084afc9 | ||
|
|
2bba3efbb1 | ||
|
|
a2af18885d | ||
|
|
edcac4e4bc | ||
|
|
71ed16ee29 | ||
|
|
f2b2f0af95 | ||
|
|
90ace9265b | ||
|
|
773e4b23b5 | ||
|
|
9a22b1238a | ||
|
|
80095603fd | ||
|
|
f234c16015 | ||
|
|
b3b1ee3252 | ||
|
|
3ba5d43c64 | ||
|
|
3720f1d235 | ||
|
|
21868d7741 | ||
|
|
ba4f93ec36 | ||
|
|
5b00846afe | ||
|
|
a5a5c35f90 | ||
|
|
5d894dcf94 | ||
|
|
5c0f9b19b0 | ||
|
|
a7d8c8f679 | ||
|
|
979ae1150c | ||
|
|
6d182f3efd | ||
|
|
8f937e4530 | ||
|
|
9dd149d20a | ||
|
|
8e96f8361a | ||
|
|
0822692903 | ||
|
|
7c4b814b43 | ||
|
|
c0b63e8564 | ||
|
|
e3ae9b37d0 | ||
|
|
95087dcba7 | ||
|
|
346c17b57d | ||
|
|
c8af89aa23 | ||
|
|
1d386e7f27 | ||
|
|
8e2d2f00e6 | ||
|
|
22d1daf3c4 | ||
|
|
32f39e2366 | ||
|
|
c7050b1979 | ||
|
|
c612637a86 | ||
|
|
f6373387fd | ||
|
|
b390d6925b | ||
|
|
8d76e923cf | ||
|
|
25b732655c | ||
|
|
f8fe1a3655 | ||
|
|
05a07567b5 | ||
|
|
9d658ef531 | ||
|
|
557b5aa480 | ||
|
|
c1140dfcc0 | ||
|
|
49e02bfbd1 | ||
|
|
f086106d53 | ||
|
|
55e55bc7a3 | ||
|
|
f9efe08984 | ||
|
|
5ff390e6fb | ||
|
|
72d2ff40f2 | ||
|
|
c667ff91be | ||
|
|
1e31fe7441 | ||
|
|
e4d1d647c5 | ||
|
|
4d078aece5 | ||
|
|
c2c73db4e7 | ||
|
|
6e736fcdac | ||
|
|
5bb5fbe468 | ||
|
|
c0da0f909f | ||
|
|
aa2b86f96d | ||
|
|
c005959835 | ||
|
|
7ea76a71f7 | ||
|
|
e89e50a3de | ||
|
|
943c5bf2ea | ||
|
|
bac6aa85c0 | ||
|
|
22540fc0ae | ||
|
|
3a335583df | ||
|
|
6668931db7 | ||
|
|
6202b45a97 | ||
|
|
2636351f5d |
@@ -25,6 +25,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Update Azure IAM service metadata to new format [(#9620)](https://github.com/prowler-cloud/prowler/pull/9620)
|
||||
- Update Azure Policy service metadata to new format [(#9625)](https://github.com/prowler-cloud/prowler/pull/9625)
|
||||
|
||||
### Fixed
|
||||
- Cloudflare `zone_waf_enabled` false positives [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896)
|
||||
|
||||
---
|
||||
|
||||
## [5.17.0] (Prowler v5.17.0)
|
||||
|
||||
@@ -32,14 +32,34 @@ class CloudflareFirewallRule(BaseModel):
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class CloudflareWAFRulesetRule(BaseModel):
|
||||
"""A rule inside a WAF managed entrypoint ruleset.
|
||||
|
||||
Each rule with ``action == "execute"`` deploys a specific managed
|
||||
ruleset identified by ``managed_ruleset_id`` (from
|
||||
``action_parameters.id``), e.g. the Cloudflare Managed Ruleset or
|
||||
OWASP Core Ruleset.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str = ""
|
||||
action: Optional[str] = None
|
||||
enabled: bool = False
|
||||
managed_ruleset_id: Optional[str] = None
|
||||
|
||||
|
||||
class CloudflareWAFRuleset(BaseModel):
|
||||
"""Represents a WAF ruleset (managed rules) for a zone."""
|
||||
"""Represents the WAF entrypoint ruleset for a zone (phase
|
||||
``http_request_firewall_managed``).
|
||||
|
||||
Contains the individual rules that deploy managed rulesets.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
kind: Optional[str] = None
|
||||
phase: Optional[str] = None
|
||||
enabled: bool = True
|
||||
rules: list[CloudflareWAFRulesetRule] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Zone(CloudflareService):
|
||||
@@ -55,7 +75,6 @@ class Zone(CloudflareService):
|
||||
self._get_zones_rate_limit_rules()
|
||||
self._get_zones_bot_management()
|
||||
self._get_zones_firewall_rules()
|
||||
self._get_zones_waf_rulesets()
|
||||
|
||||
def _list_zones(self) -> None:
|
||||
"""List all Cloudflare zones with their basic information."""
|
||||
@@ -230,7 +249,18 @@ class Zone(CloudflareService):
|
||||
)
|
||||
|
||||
def _get_zone_firewall_rules(self, zone: "CloudflareZone") -> None:
|
||||
"""List firewall rules from custom rulesets for a zone."""
|
||||
"""List firewall rules from rulesets for a zone.
|
||||
|
||||
Iterates all rulesets and, for phases relevant to security
|
||||
(custom firewall, rate-limit, WAF managed), fetches the ruleset
|
||||
detail to extract individual rules into ``zone.firewall_rules``.
|
||||
|
||||
For WAF managed rulesets (phase ``http_request_firewall_managed``)
|
||||
it also populates ``zone.waf_rulesets`` with a
|
||||
``CloudflareWAFRuleset`` containing a ``CloudflareWAFRulesetRule``
|
||||
per rule. Each ``execute`` rule references a managed ruleset via
|
||||
``action_parameters.id`` (e.g. Cloudflare Managed, OWASP Core).
|
||||
"""
|
||||
seen_ruleset_ids: set[str] = set()
|
||||
try:
|
||||
for ruleset in self.client.rulesets.list(zone_id=zone.id):
|
||||
@@ -251,11 +281,17 @@ class Zone(CloudflareService):
|
||||
)
|
||||
rules = getattr(ruleset_detail, "rules", []) or []
|
||||
seen_rule_ids: set[str] = set()
|
||||
waf_ruleset_rules: list[CloudflareWAFRulesetRule] = []
|
||||
|
||||
for rule in rules:
|
||||
rule_id = getattr(rule, "id", "")
|
||||
if rule_id in seen_rule_ids:
|
||||
break
|
||||
seen_rule_ids.add(rule_id)
|
||||
|
||||
rule_action = getattr(rule, "action", None)
|
||||
rule_enabled = getattr(rule, "enabled", True)
|
||||
|
||||
try:
|
||||
zone.firewall_rules.append(
|
||||
CloudflareFirewallRule(
|
||||
@@ -263,8 +299,8 @@ class Zone(CloudflareService):
|
||||
name=getattr(rule, "description", "")
|
||||
or rule_id,
|
||||
description=getattr(rule, "description", None),
|
||||
action=getattr(rule, "action", None),
|
||||
enabled=getattr(rule, "enabled", True),
|
||||
action=rule_action,
|
||||
enabled=rule_enabled,
|
||||
expression=getattr(rule, "expression", None),
|
||||
phase=ruleset_phase,
|
||||
)
|
||||
@@ -273,6 +309,47 @@ class Zone(CloudflareService):
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
# Collect WAF rules for the managed phase
|
||||
if ruleset_phase == "http_request_firewall_managed":
|
||||
action_params = getattr(rule, "action_parameters", None)
|
||||
managed_id = (
|
||||
getattr(action_params, "id", None)
|
||||
if action_params
|
||||
else None
|
||||
)
|
||||
try:
|
||||
waf_ruleset_rules.append(
|
||||
CloudflareWAFRulesetRule(
|
||||
id=rule_id,
|
||||
name=getattr(rule, "description", "")
|
||||
or rule_id,
|
||||
action=rule_action,
|
||||
enabled=rule_enabled,
|
||||
managed_ruleset_id=managed_id,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
# Build the WAF ruleset with its collected rules
|
||||
if ruleset_phase == "http_request_firewall_managed":
|
||||
try:
|
||||
zone.waf_rulesets.append(
|
||||
CloudflareWAFRuleset(
|
||||
id=ruleset_id,
|
||||
name=getattr(ruleset, "name", ""),
|
||||
kind=getattr(ruleset, "kind", None),
|
||||
phase=ruleset_phase,
|
||||
rules=waf_ruleset_rules,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -282,45 +359,6 @@ class Zone(CloudflareService):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_waf_rulesets(self) -> None:
|
||||
"""Get WAF rulesets for all zones."""
|
||||
logger.info("Zones - Getting WAF rulesets...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
self._get_zone_waf_rulesets(zone)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zone_waf_rulesets(self, zone: "CloudflareZone") -> None:
|
||||
"""List WAF rulesets for a zone using the rulesets API."""
|
||||
seen_ids: set[str] = set()
|
||||
try:
|
||||
for ruleset in self.client.rulesets.list(zone_id=zone.id):
|
||||
ruleset_id = getattr(ruleset, "id", "")
|
||||
if ruleset_id in seen_ids:
|
||||
break
|
||||
seen_ids.add(ruleset_id)
|
||||
try:
|
||||
zone.waf_rulesets.append(
|
||||
CloudflareWAFRuleset(
|
||||
id=ruleset_id,
|
||||
name=getattr(ruleset, "name", ""),
|
||||
kind=getattr(ruleset, "kind", None),
|
||||
phase=getattr(ruleset, "phase", None),
|
||||
enabled=True,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zone_setting(self, zone_id: str, setting_id: str):
|
||||
"""Get a single zone setting by ID."""
|
||||
try:
|
||||
@@ -342,7 +380,6 @@ class Zone(CloudflareService):
|
||||
"tls_1_3",
|
||||
"automatic_https_rewrites",
|
||||
"security_header",
|
||||
"waf",
|
||||
"security_level",
|
||||
"browser_check",
|
||||
"challenge_ttl",
|
||||
@@ -364,7 +401,6 @@ class Zone(CloudflareService):
|
||||
strict_transport_security=self._get_strict_transport_security(
|
||||
settings.get("security_header")
|
||||
),
|
||||
waf=settings.get("waf"),
|
||||
security_level=settings.get("security_level"),
|
||||
browser_check=settings.get("browser_check"),
|
||||
challenge_ttl=settings.get("challenge_ttl") or 0,
|
||||
@@ -428,7 +464,6 @@ class CloudflareZoneSettings(BaseModel):
|
||||
default_factory=StrictTransportSecurity
|
||||
)
|
||||
# Security settings
|
||||
waf: Optional[str] = None
|
||||
security_level: Optional[str] = None
|
||||
browser_check: Optional[str] = None
|
||||
challenge_ttl: Optional[int] = None
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "Zone",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**Cloudflare zones** are assessed for **Web Application Firewall (WAF)** configuration by checking if it is enabled to protect against common web vulnerabilities including **SQL injection**, **XSS**, and **OWASP Top 10** threats.",
|
||||
"Description": "**Cloudflare zones** are assessed for **Cloudflare Managed Ruleset** deployment. This managed ruleset (available on Pro, Business and Enterprise plans) provides fast and effective protection against common web vulnerabilities including **SQL injection**, **XSS**, and other threats. The Free Managed Ruleset (always active) is excluded. OWASP Core Ruleset coverage is handled by a separate check (`zone_waf_owasp_ruleset_enabled`).",
|
||||
"Risk": "Without **WAF**, web applications are exposed to common attack vectors.\n- **Confidentiality**: SQL injection attacks can exfiltrate sensitive database contents\n- **Integrity**: XSS attacks can modify page content and steal session tokens\n- **Availability**: application-layer attacks can cause service disruption",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
@@ -32,5 +32,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "WAF is available on Pro, Business, and Enterprise plans. Configure managed rulesets and create custom rules to match your application's specific security requirements."
|
||||
"Notes": "WAF Managed Ruleset is available on Pro, Business, and Enterprise plans. This check verifies deployment of the Cloudflare Managed Ruleset (efb7b8c949ac4650a09736fc376e9aee) via the managed rulesets API (phase http_request_firewall_managed). The Free Managed Ruleset (always active on all plans, cannot be disabled) is excluded. OWASP Core Ruleset coverage is handled by zone_waf_owasp_ruleset_enabled."
|
||||
}
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||
|
||||
# The Cloudflare Managed Ruleset (Pro+ plans) provides comprehensive WAF
|
||||
# protection. The Free Managed Ruleset (always active on all plans) is
|
||||
# excluded because it cannot be disabled or configured.
|
||||
# OWASP Core Ruleset coverage is handled by zone_waf_owasp_ruleset_enabled.
|
||||
CLOUDFLARE_MANAGED_RULESET_ID = "efb7b8c949ac4650a09736fc376e9aee"
|
||||
|
||||
|
||||
class zone_waf_enabled(Check):
|
||||
"""Ensure that WAF is enabled for Cloudflare zones.
|
||||
"""Ensure that the Cloudflare Managed WAF Ruleset is enabled for zones.
|
||||
|
||||
The Web Application Firewall (WAF) protects against common web vulnerabilities
|
||||
including SQL injection, cross-site scripting (XSS), and other OWASP Top 10
|
||||
threats. When enabled, it inspects HTTP requests and blocks malicious traffic
|
||||
before it reaches the origin server.
|
||||
The Cloudflare Managed Ruleset protects against common web vulnerabilities
|
||||
including SQL injection, cross-site scripting (XSS), and other threats.
|
||||
It requires a Pro, Business, or Enterprise plan.
|
||||
|
||||
The Free Managed Ruleset (available on all plans, always active) is excluded
|
||||
because it provides only basic protection and cannot be configured.
|
||||
|
||||
OWASP Core Ruleset coverage is handled separately by
|
||||
``zone_waf_owasp_ruleset_enabled``.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportCloudflare]:
|
||||
"""Execute the WAF enabled check.
|
||||
|
||||
Iterates through all Cloudflare zones and verifies that the Web Application
|
||||
Firewall is enabled. The WAF provides essential protection against common
|
||||
web application attacks.
|
||||
Iterates through all Cloudflare zones and verifies that the Cloudflare
|
||||
Managed Ruleset (``efb7b8c949ac4650a09736fc376e9aee``) is deployed and
|
||||
has at least one enabled rule.
|
||||
|
||||
Returns:
|
||||
A list of CheckReportCloudflare objects with PASS status if WAF is
|
||||
enabled, or FAIL status if it is disabled for the zone.
|
||||
A list of CheckReportCloudflare objects with PASS status if the
|
||||
Cloudflare Managed Ruleset is active, or FAIL otherwise.
|
||||
"""
|
||||
findings = []
|
||||
for zone in zone_client.zones.values():
|
||||
@@ -28,9 +39,20 @@ class zone_waf_enabled(Check):
|
||||
metadata=self.metadata(),
|
||||
resource=zone,
|
||||
)
|
||||
waf_setting = (zone.settings.waf or "").lower()
|
||||
|
||||
if waf_setting == "on":
|
||||
waf_enabled = False
|
||||
for waf_ruleset in zone.waf_rulesets:
|
||||
for rule in waf_ruleset.rules:
|
||||
if (
|
||||
rule.enabled
|
||||
and rule.managed_ruleset_id == CLOUDFLARE_MANAGED_RULESET_ID
|
||||
):
|
||||
waf_enabled = True
|
||||
break
|
||||
if waf_enabled:
|
||||
break
|
||||
|
||||
if waf_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"WAF is enabled for zone {zone.name}."
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||
|
||||
OWASP_CORE_RULESET_ID = "4814384a9e5d4991b9815dcfc25d2f1f"
|
||||
|
||||
|
||||
class zone_waf_owasp_ruleset_enabled(Check):
|
||||
"""Ensure that OWASP managed WAF rulesets are enabled for Cloudflare zones.
|
||||
@@ -14,13 +16,12 @@ class zone_waf_owasp_ruleset_enabled(Check):
|
||||
def execute(self) -> list[CheckReportCloudflare]:
|
||||
"""Execute the OWASP WAF ruleset enabled check.
|
||||
|
||||
Iterates through all Cloudflare zones and verifies that OWASP managed
|
||||
WAF rulesets are enabled. The check identifies OWASP rulesets by name
|
||||
containing "owasp" or by the http_request_firewall_managed phase.
|
||||
Iterates through all Cloudflare zones and verifies that the OWASP Core
|
||||
Ruleset (``4814384a9e5d4991b9815dcfc25d2f1f``) is deployed and enabled.
|
||||
|
||||
Returns:
|
||||
A list of CheckReportCloudflare objects with PASS status if OWASP
|
||||
rulesets are enabled, or FAIL status if no OWASP protection exists.
|
||||
ruleset is enabled, or FAIL status if no OWASP protection exists.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
@@ -30,23 +31,24 @@ class zone_waf_owasp_ruleset_enabled(Check):
|
||||
resource=zone,
|
||||
)
|
||||
|
||||
# Find OWASP managed rulesets for this zone
|
||||
# Only match rulesets that explicitly contain "owasp" in the name
|
||||
# The phase check was too broad as it matched any managed ruleset
|
||||
owasp_rulesets = [
|
||||
ruleset
|
||||
for ruleset in zone.waf_rulesets
|
||||
if "owasp" in (ruleset.name or "").lower()
|
||||
]
|
||||
# Find enabled OWASP rules by matching the well-known
|
||||
# managed ruleset ID across all WAF entrypoint rulesets.
|
||||
owasp_enabled = False
|
||||
for waf_ruleset in zone.waf_rulesets:
|
||||
for rule in waf_ruleset.rules:
|
||||
if (
|
||||
rule.enabled
|
||||
and rule.managed_ruleset_id == OWASP_CORE_RULESET_ID
|
||||
):
|
||||
owasp_enabled = True
|
||||
break
|
||||
if owasp_enabled:
|
||||
break
|
||||
|
||||
if owasp_rulesets:
|
||||
if owasp_enabled:
|
||||
report.status = "PASS"
|
||||
ruleset_descriptions = ", ".join(
|
||||
ruleset.name for ruleset in owasp_rulesets
|
||||
)
|
||||
report.status_extended = (
|
||||
f"Zone {zone.name} has OWASP managed WAF ruleset enabled: "
|
||||
f"{ruleset_descriptions}."
|
||||
f"Zone {zone.name} has OWASP managed WAF ruleset enabled."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||
CloudflareWAFRuleset,
|
||||
CloudflareWAFRulesetRule,
|
||||
CloudflareZone,
|
||||
CloudflareZoneSettings,
|
||||
)
|
||||
@@ -10,6 +12,10 @@ from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||
set_mocked_cloudflare_provider,
|
||||
)
|
||||
|
||||
CLOUDFLARE_MANAGED_ID = "efb7b8c949ac4650a09736fc376e9aee"
|
||||
OWASP_CORE_ID = "4814384a9e5d4991b9815dcfc25d2f1f"
|
||||
FREE_MANAGED_ID = "77454fe2d30c4220b5701f6fdfb893ba"
|
||||
|
||||
|
||||
class Test_zone_waf_enabled:
|
||||
def test_no_zones(self):
|
||||
@@ -34,7 +40,8 @@ class Test_zone_waf_enabled:
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_zone_waf_enabled(self):
|
||||
def test_zone_waf_enabled_with_cloudflare_managed(self):
|
||||
"""PASS when Cloudflare Managed Ruleset is deployed and enabled."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -42,9 +49,24 @@ class Test_zone_waf_enabled:
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
settings=CloudflareZoneSettings(
|
||||
waf="on",
|
||||
),
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute Cloudflare Managed Ruleset",
|
||||
action="execute",
|
||||
enabled=True,
|
||||
managed_ruleset_id=CLOUDFLARE_MANAGED_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,12 +87,11 @@ class Test_zone_waf_enabled:
|
||||
check = zone_waf_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == ZONE_ID
|
||||
assert result[0].resource_name == ZONE_NAME
|
||||
assert result[0].status == "PASS"
|
||||
assert "WAF is enabled" in result[0].status_extended
|
||||
|
||||
def test_zone_waf_disabled(self):
|
||||
def test_zone_waf_fail_only_owasp(self):
|
||||
"""FAIL when only OWASP is deployed (covered by separate check)."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -78,9 +99,73 @@ class Test_zone_waf_enabled:
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
settings=CloudflareZoneSettings(
|
||||
waf="off",
|
||||
),
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute OWASP Core Ruleset",
|
||||
action="execute",
|
||||
enabled=True,
|
||||
managed_ruleset_id=OWASP_CORE_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
def test_zone_waf_fail_only_free_ruleset(self):
|
||||
"""FAIL when only the Free Managed Ruleset is active (always-on, not configurable)."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
id=ZONE_ID,
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute Free Managed Ruleset",
|
||||
action="execute",
|
||||
enabled=True,
|
||||
managed_ruleset_id=FREE_MANAGED_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,7 +189,8 @@ class Test_zone_waf_enabled:
|
||||
assert result[0].status == "FAIL"
|
||||
assert "WAF is not enabled" in result[0].status_extended
|
||||
|
||||
def test_zone_waf_none(self):
|
||||
def test_zone_waf_fail_no_rulesets(self):
|
||||
"""FAIL when no WAF rulesets exist at all."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -112,9 +198,56 @@ class Test_zone_waf_enabled:
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
settings=CloudflareZoneSettings(
|
||||
waf=None,
|
||||
),
|
||||
settings=CloudflareZoneSettings(),
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
def test_zone_waf_fail_paid_ruleset_disabled(self):
|
||||
"""FAIL when a paid managed ruleset exists but is disabled."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
id=ZONE_ID,
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute Cloudflare Managed Ruleset",
|
||||
action="execute",
|
||||
enabled=False,
|
||||
managed_ruleset_id=CLOUDFLARE_MANAGED_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from unittest import mock
|
||||
|
||||
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||
CloudflareWAFRuleset,
|
||||
CloudflareWAFRulesetRule,
|
||||
CloudflareZone,
|
||||
CloudflareZoneSettings,
|
||||
)
|
||||
@@ -11,6 +12,9 @@ from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||
set_mocked_cloudflare_provider,
|
||||
)
|
||||
|
||||
OWASP_CORE_ID = "4814384a9e5d4991b9815dcfc25d2f1f"
|
||||
CLOUDFLARE_MANAGED_ID = "efb7b8c949ac4650a09736fc376e9aee"
|
||||
|
||||
|
||||
class Test_zone_waf_owasp_ruleset_enabled:
|
||||
def test_no_zones(self):
|
||||
@@ -35,7 +39,8 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_zone_with_owasp_ruleset_by_name(self):
|
||||
def test_zone_with_owasp_ruleset_enabled(self):
|
||||
"""PASS when the OWASP Core Ruleset is deployed and enabled."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -46,12 +51,20 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="ruleset-1",
|
||||
name="Cloudflare OWASP Core Ruleset",
|
||||
kind="managed",
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
enabled=True,
|
||||
),
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute OWASP Core Ruleset",
|
||||
action="execute",
|
||||
enabled=True,
|
||||
managed_ruleset_id=OWASP_CORE_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -73,14 +86,11 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
check = zone_waf_owasp_ruleset_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == ZONE_ID
|
||||
assert result[0].resource_name == ZONE_NAME
|
||||
assert result[0].status == "PASS"
|
||||
assert "has OWASP managed WAF ruleset enabled" in result[0].status_extended
|
||||
assert "Cloudflare OWASP Core Ruleset" in result[0].status_extended
|
||||
|
||||
def test_zone_with_managed_ruleset_without_owasp_name(self):
|
||||
"""Test that a managed ruleset without 'owasp' in name does NOT pass."""
|
||||
def test_zone_without_owasp_only_cloudflare_managed(self):
|
||||
"""FAIL when only Cloudflare Managed Ruleset is deployed (no OWASP)."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -91,12 +101,20 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="ruleset-1",
|
||||
name="Managed Rules",
|
||||
kind="managed",
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
enabled=True,
|
||||
),
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute Cloudflare Managed Ruleset",
|
||||
action="execute",
|
||||
enabled=True,
|
||||
managed_ruleset_id=CLOUDFLARE_MANAGED_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -119,12 +137,9 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
"does not have OWASP managed WAF ruleset enabled"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_zone_without_owasp_ruleset(self):
|
||||
def test_zone_with_owasp_disabled(self):
|
||||
"""FAIL when OWASP rule exists but is disabled."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -135,12 +150,20 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="ruleset-1",
|
||||
name="Custom Rules",
|
||||
kind="custom",
|
||||
phase="http_request_firewall_custom",
|
||||
enabled=True,
|
||||
),
|
||||
id="entrypoint-id",
|
||||
name="zone",
|
||||
kind="zone",
|
||||
phase="http_request_firewall_managed",
|
||||
rules=[
|
||||
CloudflareWAFRulesetRule(
|
||||
id="rule-1",
|
||||
name="Execute OWASP Core Ruleset",
|
||||
action="execute",
|
||||
enabled=False,
|
||||
managed_ruleset_id=OWASP_CORE_ID,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -163,12 +186,9 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
"does not have OWASP managed WAF ruleset enabled"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_zone_with_no_waf_rulesets(self):
|
||||
"""FAIL when no WAF rulesets exist."""
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
@@ -199,56 +219,3 @@ class Test_zone_waf_owasp_ruleset_enabled:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
"does not have OWASP managed WAF ruleset enabled"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_zone_with_multiple_owasp_rulesets(self):
|
||||
zone_client = mock.MagicMock
|
||||
zone_client.zones = {
|
||||
ZONE_ID: CloudflareZone(
|
||||
id=ZONE_ID,
|
||||
name=ZONE_NAME,
|
||||
status="active",
|
||||
paused=False,
|
||||
settings=CloudflareZoneSettings(),
|
||||
waf_rulesets=[
|
||||
CloudflareWAFRuleset(
|
||||
id="ruleset-1",
|
||||
name="Cloudflare OWASP Core Ruleset",
|
||||
kind="managed",
|
||||
phase="http_request_firewall_managed",
|
||||
enabled=True,
|
||||
),
|
||||
CloudflareWAFRuleset(
|
||||
id="ruleset-2",
|
||||
name="Custom OWASP Rules",
|
||||
kind="managed",
|
||||
phase="http_request_firewall_managed",
|
||||
enabled=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
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_owasp_ruleset_enabled.zone_waf_owasp_ruleset_enabled.zone_client",
|
||||
new=zone_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.cloudflare.services.zone.zone_waf_owasp_ruleset_enabled.zone_waf_owasp_ruleset_enabled import (
|
||||
zone_waf_owasp_ruleset_enabled,
|
||||
)
|
||||
|
||||
check = zone_waf_owasp_ruleset_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "Cloudflare OWASP Core Ruleset" in result[0].status_extended
|
||||
assert "Custom OWASP Rules" in result[0].status_extended
|
||||
|
||||
Reference in New Issue
Block a user