mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(cloudflare): Add bot protection and configuration checks for zones (#9425)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ec4eb70539
commit
aa24034ca7
@@ -23,6 +23,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
|||||||
- CIS 1.12 compliance framework for Kubernetes [(#9778)](https://github.com/prowler-cloud/prowler/pull/9778)
|
- CIS 1.12 compliance framework for Kubernetes [(#9778)](https://github.com/prowler-cloud/prowler/pull/9778)
|
||||||
- CIS 6.0 for M365 provider [(#9779)](https://github.com/prowler-cloud/prowler/pull/9779)
|
- CIS 6.0 for M365 provider [(#9779)](https://github.com/prowler-cloud/prowler/pull/9779)
|
||||||
- CIS 5.0 compliance framework for the Azure provider [(#9777)](https://github.com/prowler-cloud/prowler/pull/9777)
|
- CIS 5.0 compliance framework for the Azure provider [(#9777)](https://github.com/prowler-cloud/prowler/pull/9777)
|
||||||
|
- `Cloudflare` Bot protection, WAF, Privacy, Anti-Scraping and Zone configuration checks [(#9425)](https://github.com/prowler-cloud/prowler/pull/9425)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432)
|
- Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_always_online_disabled",
|
||||||
|
"CheckTitle": "Always Online is disabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "high",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Always Online** configuration by checking if it is disabled to prevent serving **stale cached content** when the origin server is unavailable, which could expose outdated or sensitive information.",
|
||||||
|
"Risk": "With **Always Online** enabled, Cloudflare serves cached pages when the origin is unavailable.\n- **Confidentiality**: stale cache may expose sensitive information that was subsequently removed\n- **Integrity**: outdated content may contain incorrect or superseded information\n- **Availability**: reliance on cached content masks origin failures requiring attention",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/cache/how-to/always-online/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Caching > Configuration\n3. Scroll to Always Online\n4. Toggle the setting to Off\n5. Implement proper high availability through redundant origins or load balancing",
|
||||||
|
"Terraform": "```hcl\n# Disable Always Online to prevent serving stale cached content\nresource \"cloudflare_zone_settings_override\" \"always_online\" {\n zone_id = \"<ZONE_ID>\"\n settings {\n always_online = \"off\" # Critical: prevents serving potentially stale or sensitive cached content\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Disable **Always Online** and implement proper high availability solutions.\n- Use redundant origins or load balancing for genuine high availability\n- Stale cached content may contain outdated security information\n- Origin failures should be detected and addressed, not masked\n- Consider Cloudflare Workers for custom failover logic if needed",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_always_online_disabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"resilience"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Always Online is a legacy feature that serves cached copies of pages when the origin is unreachable. Modern high availability should use redundant origins or failover configurations."
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_always_online_disabled(Check):
|
||||||
|
"""Ensure that Always Online is disabled for Cloudflare zones.
|
||||||
|
|
||||||
|
Always Online serves stale cached content when the origin server is unavailable.
|
||||||
|
While this maintains availability, it can expose outdated or potentially sensitive
|
||||||
|
information. For security-sensitive applications, it is recommended to disable
|
||||||
|
this feature to ensure users always receive current, accurate content or an
|
||||||
|
appropriate error message when the origin is down.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Always Online disabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that Always Online
|
||||||
|
is disabled. When enabled, this feature may serve stale cached content
|
||||||
|
that could contain outdated or sensitive information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if Always
|
||||||
|
Online is disabled, or FAIL status if it is enabled for the zone.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
always_online = (zone.settings.always_online or "").lower()
|
||||||
|
|
||||||
|
if always_online == "off":
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Always Online is disabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Always Online is enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_bot_fight_mode_enabled",
|
||||||
|
"CheckTitle": "Bot Fight Mode is enabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "medium",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Bot Fight Mode** configuration by checking if it is enabled to detect and mitigate **automated bot traffic** targeting the zone through browser integrity checks.",
|
||||||
|
"Risk": "Without **Bot Fight Mode**, zones are vulnerable to automated attacks.\n- **Confidentiality**: web scraping bots can harvest sensitive data from your site\n- **Integrity**: credential stuffing attacks can compromise user accounts\n- **Availability**: bot traffic can overwhelm resources causing service degradation",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/bots/get-started/free/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Security > Bots\n3. Enable Bot Fight Mode\n4. Monitor bot analytics to fine-tune protection\n5. Consider combining with rate limiting for comprehensive protection",
|
||||||
|
"Terraform": ""
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Enable **Bot Fight Mode** as part of a layered bot management strategy.\n- Detects and challenges automated bot traffic\n- Protects against web scraping and credential stuffing\n- Combine with rate limiting and WAF rules for comprehensive protection\n- Monitor bot analytics to understand traffic patterns",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_bot_fight_mode_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"internet-exposed"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Bot Fight Mode is a free feature that uses browser integrity checks to detect and challenge automated traffic. For more advanced bot management, consider Cloudflare's paid Bot Management product."
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_bot_fight_mode_enabled(Check):
|
||||||
|
"""Ensure that Bot Fight Mode is enabled for Cloudflare zones.
|
||||||
|
|
||||||
|
Bot Fight Mode is a free Cloudflare feature that detects and mitigates automated
|
||||||
|
bot traffic. It uses JavaScript challenges and behavioral analysis to identify
|
||||||
|
bots and block malicious automated traffic, protecting against scraping, spam,
|
||||||
|
credential stuffing, and other automated attacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Bot Fight Mode enabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that Bot Fight Mode
|
||||||
|
is enabled via the Bot Management API. This feature helps identify and
|
||||||
|
block malicious bot traffic.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if Bot Fight
|
||||||
|
Mode is enabled, or FAIL status if it is disabled for the zone.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
if zone.settings.bot_fight_mode_enabled:
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Bot Fight Mode is enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Bot Fight Mode is not enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_browser_integrity_check_enabled",
|
||||||
|
"CheckTitle": "Cloudflare Zone Browser Integrity Check Is Enabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "low",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Browser Integrity Check** configuration by verifying that HTTP headers are analyzed to identify requests from bots or clients with missing/invalid browser signatures.",
|
||||||
|
"Risk": "Without **Browser Integrity Check**, malformed or suspicious requests reach the origin.\n- **Confidentiality**: basic bots can access and scrape content without challenge\n- **Integrity**: requests with invalid headers may exploit application vulnerabilities\n- **Availability**: automated traffic without browser signatures consumes resources",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/waf/tools/browser-integrity-check/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Security > Settings\n3. Enable Browser Integrity Check\n4. This feature is enabled by default on most Cloudflare plans",
|
||||||
|
"Terraform": "```hcl\n# Enable Browser Integrity Check\nresource \"cloudflare_zone_settings_override\" \"browser_check\" {\n zone_id = \"<ZONE_ID>\"\n settings {\n browser_check = \"on\"\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Enable **Browser Integrity Check** to filter basic bot traffic.\n- Validates HTTP headers to identify non-browser requests\n- Challenges requests with missing or invalid browser signatures\n- Enabled by default on most Cloudflare plans\n- Low impact on legitimate users with standard browsers",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_browser_integrity_check_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"internet-exposed"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Browser Integrity Check is enabled by default on most Cloudflare plans. It provides basic protection against requests with invalid or missing browser headers."
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_browser_integrity_check_enabled(Check):
|
||||||
|
"""Ensure that Browser Integrity Check is enabled for Cloudflare zones.
|
||||||
|
|
||||||
|
Browser Integrity Check analyzes HTTP headers to identify requests from
|
||||||
|
bots or clients with missing/invalid browser signatures. It challenges
|
||||||
|
suspicious requests that don't have valid browser characteristics,
|
||||||
|
protecting against basic automated attacks and malformed requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Browser Integrity Check enabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that Browser
|
||||||
|
Integrity Check is enabled. This feature validates browser headers
|
||||||
|
to filter out basic bot traffic.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if Browser
|
||||||
|
Integrity Check is enabled, or FAIL status if it is disabled.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
browser_check = (zone.settings.browser_check or "").lower()
|
||||||
|
if browser_check == "on":
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Browser Integrity Check is enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Browser Integrity Check is not enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_challenge_passage_configured",
|
||||||
|
"CheckTitle": "Cloudflare Zone Challenge Passage Is Configured Between 15 and 45 Minutes",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "low",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Challenge Passage** (challenge TTL) configuration by checking if it is set between **15 minutes** and **45 minutes** to balance security with user experience.",
|
||||||
|
"Risk": "Improperly configured **Challenge Passage** can impact security or user experience.\n- **Confidentiality**: TTL set too long may allow attackers extended access after passing initial challenge\n- **Integrity**: security controls become less effective with overly permissive TTL settings\n- **Availability**: TTL set too short causes excessive challenges degrading user experience",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/waf/tools/challenge-passage/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Security > Settings\n3. Scroll to Challenge Passage\n4. Set the value between 15 and 45 minutes\n5. The default value of 30 minutes is recommended for most use cases",
|
||||||
|
"Terraform": "```hcl\n# Configure Challenge Passage between 15-45 minutes\nresource \"cloudflare_zone_settings_override\" \"challenge_passage\" {\n zone_id = \"<ZONE_ID>\"\n settings {\n challenge_ttl = 1800 # 30 minutes - recommended default\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Configure **Challenge Passage** between 15 and 45 minutes.\n- Values below 15 minutes may frustrate legitimate users with excessive challenges\n- Values above 45 minutes give attackers too much time after passing challenges\n- The default Cloudflare value of 30 minutes is recommended for most use cases\n- Adjust based on your specific threat model and user experience requirements",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_challenge_passage_configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"internet-exposed"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Challenge Passage determines how long a visitor who passes a challenge can access the site without being challenged again. Setting this value too low can frustrate legitimate users with excessive security challenges, while setting it too high reduces security effectiveness. The default value is 30 minutes."
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_challenge_passage_configured(Check):
|
||||||
|
"""Ensure that Challenge Passage is configured between 15 and 45 minutes for Cloudflare zones.
|
||||||
|
|
||||||
|
Challenge Passage (Challenge TTL) determines how long a visitor who has passed
|
||||||
|
a security challenge can access the site before being challenged again. A value
|
||||||
|
between 15 and 45 minutes balances security with user experience.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Challenge Passage configured check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that Challenge Passage
|
||||||
|
is set between 15 and 45 minutes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if Challenge
|
||||||
|
Passage is between 15 and 45 minutes, or FAIL status otherwise.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
min_minutes = 15
|
||||||
|
max_minutes = 45
|
||||||
|
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
# API returns seconds, convert to minutes
|
||||||
|
challenge_ttl_minutes = zone.settings.challenge_ttl // 60
|
||||||
|
|
||||||
|
if min_minutes <= challenge_ttl_minutes <= max_minutes:
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = f"Challenge Passage is set to {challenge_ttl_minutes} minutes for zone {zone.name}."
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Challenge Passage is set to {challenge_ttl_minutes} minutes for zone {zone.name} "
|
||||||
|
f"(recommended: between {min_minutes} and {max_minutes} minutes)."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_development_mode_disabled",
|
||||||
|
"CheckTitle": "Development mode is disabled for production zones",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "low",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Development Mode** configuration by checking if it is disabled to ensure **caching**, **security features**, and **performance optimizations** are active in production environments.",
|
||||||
|
"Risk": "With **Development Mode** enabled, Cloudflare bypasses caching and some optimizations.\n- **Confidentiality**: some security features may be affected or bypassed\n- **Integrity**: performance optimizations are disabled impacting site reliability\n- **Availability**: origin server is exposed to increased load without caching protection",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/cache/reference/development-mode/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Caching > Configuration\n3. Scroll to Development Mode\n4. Ensure Development Mode is Off\n5. Note: Development Mode auto-expires after 3 hours if left enabled",
|
||||||
|
"Terraform": ""
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Disable **Development Mode** for production environments.\n- Use only temporarily during active development when cache bypassing is necessary\n- Development Mode auto-expires after 3 hours\n- Ensure it is manually disabled after development work completes\n- Consider using cache purge or page rules for targeted cache invalidation instead",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_development_mode_disabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"resilience"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Development Mode temporarily suspends Cloudflare caching and minification. It auto-expires after 3 hours to prevent accidental prolonged use."
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_development_mode_disabled(Check):
|
||||||
|
"""Ensure that Development Mode is disabled for production Cloudflare zones.
|
||||||
|
|
||||||
|
Development Mode temporarily bypasses Cloudflare's caching and performance
|
||||||
|
optimizations, serving content directly from the origin server. While useful
|
||||||
|
for testing changes, it should be disabled in production to maintain caching,
|
||||||
|
security features, and performance optimizations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Development Mode disabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that Development Mode
|
||||||
|
is disabled. When enabled, this mode bypasses caching and can impact
|
||||||
|
performance and security.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if Development
|
||||||
|
Mode is disabled, or FAIL status if it is enabled for the zone.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
dev_mode = (zone.settings.development_mode or "").lower()
|
||||||
|
if dev_mode == "off" or not dev_mode:
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Development mode is disabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Development mode is enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_hotlink_protection_enabled",
|
||||||
|
"CheckTitle": "Hotlink Protection is enabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "low",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Hotlink Protection** (Scrape Shield) configuration by checking if it is enabled to prevent other websites from directly linking to **images and media**, consuming bandwidth without authorization.",
|
||||||
|
"Risk": "Without **Hotlink Protection**, external websites can embed your media directly.\n- **Confidentiality**: content may be used without proper attribution or permission\n- **Integrity**: unauthorized use of media may misrepresent your brand\n- **Availability**: bandwidth theft increases costs and may degrade performance",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/waf/tools/scrape-shield/hotlink-protection/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Scrape Shield (or Security > Settings in newer UI)\n3. Scroll to Hotlink Protection\n4. Toggle the setting to On\n5. Review allowed referrers if legitimate integrations require access",
|
||||||
|
"Terraform": "```hcl\n# Enable Hotlink Protection to prevent bandwidth theft\nresource \"cloudflare_zone_settings_override\" \"hotlink_protection\" {\n zone_id = \"<ZONE_ID>\"\n settings {\n hotlink_protection = \"on\" # Blocks unauthorized embedding of your media resources\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Enable **Hotlink Protection** to prevent unauthorized embedding of your media.\n- Blocks requests to images when HTTP referer does not match your domain\n- Reduces bandwidth costs from unauthorized embedding\n- Review allowed referrers for legitimate integrations\n- Part of the Scrape Shield feature set for comprehensive protection",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_hotlink_protection_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"internet-exposed"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Hotlink Protection blocks requests to images when the HTTP referer does not match your domain. May need configuration for legitimate third-party embedding use cases."
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_hotlink_protection_enabled(Check):
|
||||||
|
"""Ensure that Hotlink Protection is enabled for Cloudflare zones.
|
||||||
|
|
||||||
|
Hotlink Protection is part of Cloudflare's Scrape Shield suite that prevents
|
||||||
|
other websites from directly linking to images, videos, and other media files,
|
||||||
|
which consumes bandwidth without authorization. It blocks requests where the
|
||||||
|
HTTP referer does not match your domain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Hotlink Protection enabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that Hotlink Protection
|
||||||
|
is enabled. This feature prevents bandwidth theft by blocking unauthorized
|
||||||
|
embedding of your media on external sites.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if Hotlink
|
||||||
|
Protection is enabled, or FAIL status if it is disabled for the zone.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
hotlink_protection = (zone.settings.hotlink_protection or "").lower()
|
||||||
|
if hotlink_protection == "on":
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Hotlink Protection is enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Hotlink Protection is not enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_ip_geolocation_enabled",
|
||||||
|
"CheckTitle": "IP Geolocation is enabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "low",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **IP Geolocation** configuration by checking if it is enabled to add the **CF-IPCountry header** to requests, enabling geographic-based access controls, firewall rules, and analytics.",
|
||||||
|
"Risk": "Without **IP Geolocation**, geographic-based security controls cannot be implemented.\n- **Confidentiality**: unable to restrict access from high-risk regions\n- **Integrity**: cannot enforce geographic data residency requirements\n- **Availability**: limited visibility into traffic origins for threat analysis",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/network/ip-geolocation/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Network\n3. Scroll to IP Geolocation\n4. Toggle the setting to On\n5. Use the CF-IPCountry header in your firewall rules or application logic",
|
||||||
|
"Terraform": "```hcl\n# Enable IP Geolocation for geographic-based security controls\nresource \"cloudflare_zone_settings_override\" \"ip_geolocation\" {\n zone_id = \"<ZONE_ID>\"\n settings {\n ip_geolocation = \"on\" # Adds CF-IPCountry header for geo-based controls\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Enable **IP Geolocation** to support geographic-based security policies.\n- Adds CF-IPCountry header to all requests\n- Enables geo-blocking rules in firewall configuration\n- Provides visibility into traffic origins for threat analysis\n- Essential for geographic data residency compliance",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_ip_geolocation_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"internet-exposed"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "IP Geolocation is essential for implementing geo-blocking or country-specific security rules. The CF-IPCountry header contains ISO 3166-1 Alpha 2 country codes."
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_ip_geolocation_enabled(Check):
|
||||||
|
"""Ensure that IP Geolocation is enabled for Cloudflare zones.
|
||||||
|
|
||||||
|
IP Geolocation adds the CF-IPCountry header to all requests, containing the
|
||||||
|
two-letter country code of the visitor's location. This enables geographic-based
|
||||||
|
access controls, firewall rules, content customization, and analytics based on
|
||||||
|
visitor location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the IP Geolocation enabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that IP Geolocation
|
||||||
|
is enabled. This feature adds geographic information to requests for
|
||||||
|
enhanced security controls and analytics.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if IP
|
||||||
|
Geolocation is enabled, or FAIL status if it is disabled for the zone.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
ip_geolocation = (zone.settings.ip_geolocation or "").lower()
|
||||||
|
|
||||||
|
if ip_geolocation == "on":
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = (
|
||||||
|
f"IP Geolocation is enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"IP Geolocation is not enabled for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_rate_limiting_enabled",
|
||||||
|
"CheckTitle": "Rate limiting is configured for the zone",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "high",
|
||||||
|
"ResourceType": "Zone",
|
||||||
|
"ResourceGroup": "network",
|
||||||
|
"Description": "**Cloudflare zones** are assessed for **Rate Limiting** configuration by checking if rules are configured to protect against **DDoS attacks**, **brute force attempts**, and **API abuse**.",
|
||||||
|
"Risk": "Without **Rate Limiting**, applications are vulnerable to volumetric attacks.\n- **Confidentiality**: credential brute forcing can compromise user accounts\n- **Integrity**: API abuse can manipulate data through excessive requests\n- **Availability**: volumetric attacks can exhaust resources causing service degradation",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://developers.cloudflare.com/waf/rate-limiting-rules/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Security > WAF > Rate limiting rules\n3. Create a new rate limiting rule\n4. Configure thresholds based on expected traffic patterns\n5. Apply stricter limits to sensitive endpoints like authentication and APIs",
|
||||||
|
"Terraform": "```hcl\n# Configure Rate Limiting to protect against volumetric attacks\nresource \"cloudflare_ruleset\" \"rate_limit\" {\n zone_id = \"<ZONE_ID>\"\n name = \"Rate limiting\"\n kind = \"zone\"\n phase = \"http_ratelimit\"\n rules {\n action = \"block\"\n ratelimit {\n characteristics = [\"ip.src\"]\n period = 60\n requests_per_period = 100\n mitigation_timeout = 600\n }\n expression = \"true\"\n description = \"Rate limit all requests\"\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Implement **Rate Limiting** as part of defense in depth.\n- Configure thresholds based on expected traffic patterns\n- Apply stricter limits to sensitive endpoints like authentication and APIs\n- Use different rate limits for different paths or endpoints\n- Monitor rate limiting analytics to fine-tune thresholds",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_rate_limiting_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"internet-exposed"
|
||||||
|
],
|
||||||
|
"DependsOn": [],
|
||||||
|
"RelatedTo": [],
|
||||||
|
"Notes": "Rate limiting rules are configured in the http_ratelimit phase. Consider different thresholds for different types of endpoints based on their sensitivity and expected usage patterns."
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_rate_limiting_enabled(Check):
|
||||||
|
"""Ensure that Rate Limiting is configured for Cloudflare zones.
|
||||||
|
|
||||||
|
Rate Limiting protects against DDoS attacks, brute force attempts, and API
|
||||||
|
abuse by limiting the number of requests from a single source within a specified
|
||||||
|
time window. Rules are configured in the http_ratelimit phase and help maintain
|
||||||
|
service availability under high-traffic conditions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[CheckReportCloudflare]:
|
||||||
|
"""Execute the Rate Limiting enabled check.
|
||||||
|
|
||||||
|
Iterates through all Cloudflare zones and verifies that at least one
|
||||||
|
enabled rate limiting rule exists.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if rate
|
||||||
|
limiting rules are configured, or FAIL status if no rules exist.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get enabled rate limiting rules for this zone
|
||||||
|
enabled_rules = [rule for rule in zone.rate_limit_rules if rule.enabled]
|
||||||
|
|
||||||
|
if enabled_rules:
|
||||||
|
report.status = "PASS"
|
||||||
|
rules_str = ", ".join(
|
||||||
|
rule.description or rule.id for rule in enabled_rules
|
||||||
|
)
|
||||||
|
report.status_extended = (
|
||||||
|
f"Rate limiting is configured for zone {zone.name}: {rules_str}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"No rate limiting rules configured for zone {zone.name}."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
|
||||||
|
return findings
|
||||||
@@ -7,6 +7,16 @@ from prowler.providers.cloudflare.lib.service.service import CloudflareService
|
|||||||
from prowler.providers.cloudflare.models import CloudflareAccount
|
from prowler.providers.cloudflare.models import CloudflareAccount
|
||||||
|
|
||||||
|
|
||||||
|
class CloudflareRateLimitRule(BaseModel):
|
||||||
|
"""Cloudflare rate limiting rule representation."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
action: Optional[str] = None
|
||||||
|
enabled: bool = True
|
||||||
|
expression: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Zone(CloudflareService):
|
class Zone(CloudflareService):
|
||||||
"""Retrieve Cloudflare zones with security-relevant settings."""
|
"""Retrieve Cloudflare zones with security-relevant settings."""
|
||||||
|
|
||||||
@@ -17,6 +27,8 @@ class Zone(CloudflareService):
|
|||||||
self._get_zones_settings()
|
self._get_zones_settings()
|
||||||
self._get_zones_dnssec()
|
self._get_zones_dnssec()
|
||||||
self._get_zones_universal_ssl()
|
self._get_zones_universal_ssl()
|
||||||
|
self._get_zones_rate_limit_rules()
|
||||||
|
self._get_zones_bot_management()
|
||||||
|
|
||||||
def _list_zones(self) -> None:
|
def _list_zones(self) -> None:
|
||||||
"""List all Cloudflare zones with their basic information."""
|
"""List all Cloudflare zones with their basic information."""
|
||||||
@@ -122,6 +134,63 @@ class Zone(CloudflareService):
|
|||||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_zones_rate_limit_rules(self) -> None:
|
||||||
|
"""Get rate limiting rules for all zones."""
|
||||||
|
logger.info("Zone - Getting rate limit rules...")
|
||||||
|
for zone in self.zones.values():
|
||||||
|
try:
|
||||||
|
seen_ruleset_ids: set[str] = set()
|
||||||
|
for ruleset in self.client.rulesets.list(zone_id=zone.id):
|
||||||
|
ruleset_id = getattr(ruleset, "id", "")
|
||||||
|
if ruleset_id in seen_ruleset_ids:
|
||||||
|
break
|
||||||
|
seen_ruleset_ids.add(ruleset_id)
|
||||||
|
|
||||||
|
phase = getattr(ruleset, "phase", "")
|
||||||
|
if phase == "http_ratelimit":
|
||||||
|
try:
|
||||||
|
ruleset_detail = self.client.rulesets.get(
|
||||||
|
ruleset_id=ruleset_id, zone_id=zone.id
|
||||||
|
)
|
||||||
|
rules = getattr(ruleset_detail, "rules", []) or []
|
||||||
|
seen_rule_ids: set[str] = set()
|
||||||
|
for rule in rules:
|
||||||
|
rule_id = getattr(rule, "id", "")
|
||||||
|
if rule_id in seen_rule_ids:
|
||||||
|
break
|
||||||
|
seen_rule_ids.add(rule_id)
|
||||||
|
zone.rate_limit_rules.append(
|
||||||
|
CloudflareRateLimitRule(
|
||||||
|
id=rule_id,
|
||||||
|
description=getattr(rule, "description", None),
|
||||||
|
action=getattr(rule, "action", None),
|
||||||
|
enabled=getattr(rule, "enabled", True),
|
||||||
|
expression=getattr(rule, "expression", None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
logger.debug(
|
||||||
|
f"{zone.id} ruleset {ruleset_id} -- {error.__class__.__name__}: {error}"
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(
|
||||||
|
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_zones_bot_management(self) -> None:
|
||||||
|
"""Get Bot Management settings for all zones."""
|
||||||
|
logger.info("Zone - Getting Bot Management settings...")
|
||||||
|
for zone in self.zones.values():
|
||||||
|
try:
|
||||||
|
bot_management = self.client.bot_management.get(zone_id=zone.id)
|
||||||
|
zone.settings.bot_fight_mode_enabled = getattr(
|
||||||
|
bot_management, "fight_mode", False
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(
|
||||||
|
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||||
|
)
|
||||||
|
|
||||||
def _get_zone_setting(self, zone_id: str, setting_id: str):
|
def _get_zone_setting(self, zone_id: str, setting_id: str):
|
||||||
"""Get a single zone setting by ID."""
|
"""Get a single zone setting by ID."""
|
||||||
try:
|
try:
|
||||||
@@ -168,7 +237,7 @@ class Zone(CloudflareService):
|
|||||||
waf=settings.get("waf"),
|
waf=settings.get("waf"),
|
||||||
security_level=settings.get("security_level"),
|
security_level=settings.get("security_level"),
|
||||||
browser_check=settings.get("browser_check"),
|
browser_check=settings.get("browser_check"),
|
||||||
challenge_ttl=settings.get("challenge_ttl"),
|
challenge_ttl=settings.get("challenge_ttl") or 0,
|
||||||
ip_geolocation=settings.get("ip_geolocation"),
|
ip_geolocation=settings.get("ip_geolocation"),
|
||||||
email_obfuscation=settings.get("email_obfuscation"),
|
email_obfuscation=settings.get("email_obfuscation"),
|
||||||
server_side_exclude=settings.get("server_side_exclude"),
|
server_side_exclude=settings.get("server_side_exclude"),
|
||||||
@@ -241,6 +310,8 @@ class CloudflareZoneSettings(BaseModel):
|
|||||||
# Zone state
|
# Zone state
|
||||||
development_mode: Optional[str] = None
|
development_mode: Optional[str] = None
|
||||||
always_online: Optional[str] = None
|
always_online: Optional[str] = None
|
||||||
|
# Bot management
|
||||||
|
bot_fight_mode_enabled: bool = False
|
||||||
|
|
||||||
|
|
||||||
class CloudflareZone(BaseModel):
|
class CloudflareZone(BaseModel):
|
||||||
@@ -254,3 +325,4 @@ class CloudflareZone(BaseModel):
|
|||||||
plan: Optional[str] = None
|
plan: Optional[str] = None
|
||||||
settings: CloudflareZoneSettings = Field(default_factory=CloudflareZoneSettings)
|
settings: CloudflareZoneSettings = Field(default_factory=CloudflareZoneSettings)
|
||||||
dnssec_status: Optional[str] = None
|
dnssec_status: Optional[str] = None
|
||||||
|
rate_limit_rules: list[CloudflareRateLimitRule] = Field(default_factory=list)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"Provider": "cloudflare",
|
||||||
|
"CheckID": "zone_waf_enabled",
|
||||||
|
"CheckTitle": "WAF is enabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "zone",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"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.",
|
||||||
|
"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": [
|
||||||
|
"https://developers.cloudflare.com/waf/"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Log in to the Cloudflare dashboard and select your account and domain\n2. Go to Security > WAF\n3. Enable managed rulesets (OWASP, Cloudflare Managed Ruleset)\n4. Configure custom rules based on your application's specific threat model\n5. Monitor WAF analytics to tune rules and reduce false positives",
|
||||||
|
"Terraform": "```hcl\n# Enable WAF managed rulesets for comprehensive protection\nresource \"cloudflare_ruleset\" \"waf_managed\" {\n zone_id = \"<ZONE_ID>\"\n name = \"WAF Managed Rules\"\n kind = \"zone\"\n phase = \"http_request_firewall_managed\"\n rules {\n action = \"execute\"\n action_parameters {\n id = \"efb7b8c949ac4650a09736fc376e9aee\" # Cloudflare Managed Ruleset\n }\n expression = \"true\"\n description = \"Execute Cloudflare Managed Ruleset\"\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Enable **WAF** as a critical layer of defense for web applications.\n- Deploy managed rulesets for protection against OWASP Top 10 vulnerabilities\n- Create custom rules based on your application's specific threat model\n- Monitor WAF analytics to tune rules and reduce false positives\n- Combine with rate limiting and bot protection for comprehensive security",
|
||||||
|
"Url": "https://hub.prowler.com/checks/cloudflare/zone_waf_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"vulnerabilities"
|
||||||
|
],
|
||||||
|
"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."
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from prowler.lib.check.models import Check, CheckReportCloudflare
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
|
||||||
|
|
||||||
|
|
||||||
|
class zone_waf_enabled(Check):
|
||||||
|
"""Ensure that WAF is enabled for Cloudflare 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of CheckReportCloudflare objects with PASS status if WAF is
|
||||||
|
enabled, or FAIL status if it is disabled for the zone.
|
||||||
|
"""
|
||||||
|
findings = []
|
||||||
|
for zone in zone_client.zones.values():
|
||||||
|
report = CheckReportCloudflare(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=zone,
|
||||||
|
)
|
||||||
|
waf_setting = (zone.settings.waf or "").lower()
|
||||||
|
|
||||||
|
if waf_setting == "on":
|
||||||
|
report.status = "PASS"
|
||||||
|
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}."
|
||||||
|
findings.append(report)
|
||||||
|
return findings
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_always_online_disabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_always_online_disabled.zone_always_online_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_always_online_disabled.zone_always_online_disabled import (
|
||||||
|
zone_always_online_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_always_online_disabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_always_online_disabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
always_online="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_always_online_disabled.zone_always_online_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_always_online_disabled.zone_always_online_disabled import (
|
||||||
|
zone_always_online_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_always_online_disabled()
|
||||||
|
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 "Always Online is disabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_always_online_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
always_online="on",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_always_online_disabled.zone_always_online_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_always_online_disabled.zone_always_online_disabled import (
|
||||||
|
zone_always_online_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_always_online_disabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "Always Online is enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_always_online_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
always_online=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_always_online_disabled.zone_always_online_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_always_online_disabled.zone_always_online_disabled import (
|
||||||
|
zone_always_online_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_always_online_disabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_bot_fight_mode_enabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_bot_fight_mode_enabled.zone_bot_fight_mode_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_bot_fight_mode_enabled.zone_bot_fight_mode_enabled import (
|
||||||
|
zone_bot_fight_mode_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_bot_fight_mode_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_bot_fight_mode_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
bot_fight_mode_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_bot_fight_mode_enabled.zone_bot_fight_mode_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_bot_fight_mode_enabled.zone_bot_fight_mode_enabled import (
|
||||||
|
zone_bot_fight_mode_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_bot_fight_mode_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 "Bot Fight Mode" in result[0].status_extended
|
||||||
|
assert "enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_bot_fight_mode_disabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
bot_fight_mode_enabled=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_bot_fight_mode_enabled.zone_bot_fight_mode_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_bot_fight_mode_enabled.zone_bot_fight_mode_enabled import (
|
||||||
|
zone_bot_fight_mode_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_bot_fight_mode_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "not enabled" in result[0].status_extended
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_browser_integrity_check_enabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_browser_integrity_check_enabled.zone_browser_integrity_check_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_browser_integrity_check_enabled.zone_browser_integrity_check_enabled import (
|
||||||
|
zone_browser_integrity_check_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_browser_integrity_check_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_browser_integrity_check_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
browser_check="on",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_browser_integrity_check_enabled.zone_browser_integrity_check_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_browser_integrity_check_enabled.zone_browser_integrity_check_enabled import (
|
||||||
|
zone_browser_integrity_check_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_browser_integrity_check_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 "Browser Integrity Check" in result[0].status_extended
|
||||||
|
assert "enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_browser_integrity_check_disabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
browser_check="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_browser_integrity_check_enabled.zone_browser_integrity_check_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_browser_integrity_check_enabled.zone_browser_integrity_check_enabled import (
|
||||||
|
zone_browser_integrity_check_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_browser_integrity_check_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "not enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_browser_integrity_check_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
browser_check=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_browser_integrity_check_enabled.zone_browser_integrity_check_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_browser_integrity_check_enabled.zone_browser_integrity_check_enabled import (
|
||||||
|
zone_browser_integrity_check_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_browser_integrity_check_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_challenge_passage_configured:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_challenge_passage_at_min(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
challenge_ttl=900, # 15 minutes - minimum recommended
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
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 "15 minutes" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_challenge_passage_at_max(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
challenge_ttl=2700, # 45 minutes - maximum recommended
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "PASS"
|
||||||
|
assert "45 minutes" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_challenge_passage_default(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
challenge_ttl=1800, # 30 minutes - default and secure
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "PASS"
|
||||||
|
assert "30 minutes" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_challenge_passage_too_short(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
challenge_ttl=300, # 5 minutes - too short
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "5 minutes" in result[0].status_extended
|
||||||
|
assert "recommended" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_challenge_passage_too_long(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
challenge_ttl=3600, # 60 minutes - exceeds recommended
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "60 minutes" in result[0].status_extended
|
||||||
|
assert "recommended" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_challenge_passage_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
challenge_ttl=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_challenge_passage_configured.zone_challenge_passage_configured.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_challenge_passage_configured.zone_challenge_passage_configured import (
|
||||||
|
zone_challenge_passage_configured,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_challenge_passage_configured()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_development_mode_disabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_development_mode_disabled.zone_development_mode_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_development_mode_disabled.zone_development_mode_disabled import (
|
||||||
|
zone_development_mode_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_development_mode_disabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_development_mode_disabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
development_mode="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_development_mode_disabled.zone_development_mode_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_development_mode_disabled.zone_development_mode_disabled import (
|
||||||
|
zone_development_mode_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_development_mode_disabled()
|
||||||
|
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 "Development mode is disabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_development_mode_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
development_mode="on",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_development_mode_disabled.zone_development_mode_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_development_mode_disabled.zone_development_mode_disabled import (
|
||||||
|
zone_development_mode_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_development_mode_disabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "Development mode is enabled" in result[0].status_extended
|
||||||
|
assert "bypasses" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_development_mode_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
development_mode=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_development_mode_disabled.zone_development_mode_disabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_development_mode_disabled.zone_development_mode_disabled import (
|
||||||
|
zone_development_mode_disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_development_mode_disabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
# None or empty string should be treated as disabled (PASS)
|
||||||
|
assert result[0].status == "PASS"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_hotlink_protection_enabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_hotlink_protection_enabled.zone_hotlink_protection_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_hotlink_protection_enabled.zone_hotlink_protection_enabled import (
|
||||||
|
zone_hotlink_protection_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_hotlink_protection_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_hotlink_protection_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
hotlink_protection="on",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_hotlink_protection_enabled.zone_hotlink_protection_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_hotlink_protection_enabled.zone_hotlink_protection_enabled import (
|
||||||
|
zone_hotlink_protection_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_hotlink_protection_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 "Hotlink Protection is enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_hotlink_protection_disabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
hotlink_protection="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_hotlink_protection_enabled.zone_hotlink_protection_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_hotlink_protection_enabled.zone_hotlink_protection_enabled import (
|
||||||
|
zone_hotlink_protection_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_hotlink_protection_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "Hotlink Protection is not enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_hotlink_protection_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
hotlink_protection=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_hotlink_protection_enabled.zone_hotlink_protection_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_hotlink_protection_enabled.zone_hotlink_protection_enabled import (
|
||||||
|
zone_hotlink_protection_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_hotlink_protection_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_ip_geolocation_enabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_ip_geolocation_enabled.zone_ip_geolocation_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_ip_geolocation_enabled.zone_ip_geolocation_enabled import (
|
||||||
|
zone_ip_geolocation_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_ip_geolocation_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_ip_geolocation_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
ip_geolocation="on",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_ip_geolocation_enabled.zone_ip_geolocation_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_ip_geolocation_enabled.zone_ip_geolocation_enabled import (
|
||||||
|
zone_ip_geolocation_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_ip_geolocation_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 "IP Geolocation is enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_ip_geolocation_disabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
ip_geolocation="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_ip_geolocation_enabled.zone_ip_geolocation_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_ip_geolocation_enabled.zone_ip_geolocation_enabled import (
|
||||||
|
zone_ip_geolocation_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_ip_geolocation_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "IP Geolocation is not enabled" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_ip_geolocation_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
ip_geolocation=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_ip_geolocation_enabled.zone_ip_geolocation_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_ip_geolocation_enabled.zone_ip_geolocation_enabled import (
|
||||||
|
zone_ip_geolocation_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_ip_geolocation_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareRateLimitRule,
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_rate_limiting_enabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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_rate_limiting_enabled.zone_rate_limiting_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_rate_limiting_enabled.zone_rate_limiting_enabled import (
|
||||||
|
zone_rate_limiting_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_rate_limiting_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_zone_with_rate_limiting_rules(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(),
|
||||||
|
rate_limit_rules=[
|
||||||
|
CloudflareRateLimitRule(
|
||||||
|
id="rule-1",
|
||||||
|
description="API Rate Limit",
|
||||||
|
action="block",
|
||||||
|
enabled=True,
|
||||||
|
expression="(http.request.uri.path contains '/api/')",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_rate_limiting_enabled.zone_rate_limiting_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_rate_limiting_enabled.zone_rate_limiting_enabled import (
|
||||||
|
zone_rate_limiting_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_rate_limiting_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 "Rate limiting is configured" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_with_multiple_rate_limiting_rules(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(),
|
||||||
|
rate_limit_rules=[
|
||||||
|
CloudflareRateLimitRule(
|
||||||
|
id="rule-1",
|
||||||
|
description="API Rate Limit",
|
||||||
|
enabled=True,
|
||||||
|
),
|
||||||
|
CloudflareRateLimitRule(
|
||||||
|
id="rule-2",
|
||||||
|
description="Login Rate Limit",
|
||||||
|
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_rate_limiting_enabled.zone_rate_limiting_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_rate_limiting_enabled.zone_rate_limiting_enabled import (
|
||||||
|
zone_rate_limiting_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_rate_limiting_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "PASS"
|
||||||
|
|
||||||
|
def test_zone_without_rate_limiting_rules(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(),
|
||||||
|
rate_limit_rules=[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_rate_limiting_enabled.zone_rate_limiting_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_rate_limiting_enabled.zone_rate_limiting_enabled import (
|
||||||
|
zone_rate_limiting_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_rate_limiting_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert "No rate limiting rules configured" in result[0].status_extended
|
||||||
|
|
||||||
|
def test_zone_with_disabled_rate_limiting_rules(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(),
|
||||||
|
rate_limit_rules=[
|
||||||
|
CloudflareRateLimitRule(
|
||||||
|
id="rule-1",
|
||||||
|
description="Disabled Rule",
|
||||||
|
enabled=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_rate_limiting_enabled.zone_rate_limiting_enabled.zone_client",
|
||||||
|
new=zone_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_rate_limiting_enabled.zone_rate_limiting_enabled import (
|
||||||
|
zone_rate_limiting_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = zone_rate_limiting_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.cloudflare.services.zone.zone_service import (
|
||||||
|
CloudflareZone,
|
||||||
|
CloudflareZoneSettings,
|
||||||
|
)
|
||||||
|
from tests.providers.cloudflare.cloudflare_fixtures import (
|
||||||
|
ZONE_ID,
|
||||||
|
ZONE_NAME,
|
||||||
|
set_mocked_cloudflare_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_zone_waf_enabled:
|
||||||
|
def test_no_zones(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {}
|
||||||
|
|
||||||
|
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) == 0
|
||||||
|
|
||||||
|
def test_zone_waf_enabled(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
waf="on",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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].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):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
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
|
||||||
|
|
||||||
|
def test_zone_waf_none(self):
|
||||||
|
zone_client = mock.MagicMock
|
||||||
|
zone_client.zones = {
|
||||||
|
ZONE_ID: CloudflareZone(
|
||||||
|
id=ZONE_ID,
|
||||||
|
name=ZONE_NAME,
|
||||||
|
status="active",
|
||||||
|
paused=False,
|
||||||
|
settings=CloudflareZoneSettings(
|
||||||
|
waf=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
Reference in New Issue
Block a user