Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
4 changes: 4 additions & 0 deletions prowler/providers/cloudflare/services/dns/dns_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from prowler.providers.cloudflare.services.dns.dns_service import DNS
from prowler.providers.common.provider import Provider

dns_client = DNS(Provider.get_global_provider())
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Provider": "cloudflare",
"CheckID": "dns_records_proxied",
"CheckTitle": "Cloudflare proxy is enabled for applicable DNS records",
"CheckType": [],
"ServiceName": "dns",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "DNSRecord",
"Description": "Verifies that A, AAAA, and CNAME DNS records are proxied through Cloudflare to benefit from DDoS protection, WAF, and caching capabilities.",
"Risk": "Unproxied DNS records expose origin server IP addresses directly to the internet, bypassing Cloudflare's security protections and increasing the attack surface for direct attacks against the origin infrastructure.",
"RelatedUrl": "https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the Cloudflare proxy (orange cloud) for DNS records that should be protected. Apply defense in depth by combining proxy protection with origin server hardening and access controls.",
"Url": "https://hub.prowler.com/checks/dns_records_proxied"
}
},
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from prowler.lib.check.models import Check, CheckReportCloudflare
from prowler.providers.cloudflare.services.dns.dns_client import dns_client

PROXYABLE_TYPES = {"A", "AAAA", "CNAME"}


class dns_records_proxied(Check):
def execute(self) -> list[CheckReportCloudflare]:
findings = []

for record in dns_client.records:
# Only check proxyable record types
if record.type not in PROXYABLE_TYPES:
continue

report = CheckReportCloudflare(
metadata=self.metadata(),
resource=record,
zone=record.zone,
)

if record.proxied:
report.status = "PASS"
report.status_extended = f"DNS record '{record.name}' ({record.type}) is proxied through Cloudflare."
else:
report.status = "FAIL"
report.status_extended = f"DNS record '{record.name}' ({record.type}) is not proxied through Cloudflare."
findings.append(report)

return findings
75 changes: 75 additions & 0 deletions prowler/providers/cloudflare/services/dns/dns_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Optional

from pydantic.v1 import BaseModel

from prowler.lib.logger import logger
from prowler.providers.cloudflare.lib.service.service import CloudflareService
from prowler.providers.cloudflare.models import CloudflareZone


class CloudflareDNSRecord(BaseModel):
"""Represents a DNS record."""

id: str
name: str
type: str
content: str
proxied: bool = False
ttl: Optional[int] = None
zone: CloudflareZone

class Config:
arbitrary_types_allowed = True


class DNS(CloudflareService):
"""Collect DNS records for each Cloudflare zone."""

def __init__(self, provider):
super().__init__(__class__.__name__, provider)
self.records: list[CloudflareDNSRecord] = []
self.__threading_call__(self._list_zone_records)
if self.records:
record_types = {}
for r in self.records:
record_types[r.type] = record_types.get(r.type, 0) + 1
types_summary = ", ".join(
f"{t}: {c}" for t, c in sorted(record_types.items())
)
logger.info(
f"DNS service collected {len(self.records)} record(s) across {len(self.zones)} zone(s) - Types: {types_summary}"
)
else:
logger.info(
f"DNS service collected 0 records across {len(self.zones)} zone(s)"
)

def _list_zone_records(self, zone: CloudflareZone):
"""List all DNS records for a zone."""
seen_ids: set[str] = set()
try:
for record in self.client.dns.records.list(zone_id=zone.id):
record_id = getattr(record, "id", "")
if record_id in seen_ids:
break
seen_ids.add(record_id)
try:
self.records.append(
CloudflareDNSRecord(
id=record_id,
name=getattr(record, "name", ""),
type=getattr(record, "type", ""),
content=getattr(record, "content", ""),
proxied=getattr(record, "proxied", False),
ttl=getattr(record, "ttl", None),
zone=zone,
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from prowler.providers.cloudflare.services.firewall.firewall_service import Firewall
from prowler.providers.common.provider import Provider

firewall_client = Firewall(Provider.get_global_provider())
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"Provider": "cloudflare",
"CheckID": "firewall_has_blocking_rules",
"CheckTitle": "Firewall rules use blocking actions to protect against threats",
"CheckType": [],
"ServiceName": "firewall",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "FirewallRule",
"Description": "Verifies that Cloudflare firewall rules use blocking actions (block, challenge, js_challenge, managed_challenge) to actively protect against threats rather than only logging.",
"Risk": "Firewall rules configured only for logging provide visibility but no protection. Malicious traffic reaches the origin server, enabling attacks such as credential stuffing, application exploits, and data exfiltration.",
"RelatedUrl": "https://developers.cloudflare.com/waf/custom-rules/",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": "resource \"cloudflare_ruleset\" \"example\" {\n zone_id = var.zone_id\n name = \"Block malicious requests\"\n kind = \"zone\"\n phase = \"http_request_firewall_custom\"\n rules {\n action = \"block\"\n expression = \"(ip.geoip.country eq \\\"XX\\\")\"\n description = \"Block traffic from country XX\"\n }\n}"
},
"Recommendation": {
"Text": "Configure firewall rules with blocking actions to enforce security policies. Use challenge actions for suspicious traffic and block actions for known malicious patterns following the principle of least privilege.",
"Url": "https://hub.prowler.com/checks/firewall_has_blocking_rules"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Blocking actions include: block, challenge, js_challenge, managed_challenge."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from prowler.lib.check.models import Check, CheckReportCloudflare
from prowler.providers.cloudflare.services.firewall.firewall_client import (
firewall_client,
)

BLOCKING_ACTIONS = {"block", "challenge", "js_challenge", "managed_challenge"}


class firewall_has_blocking_rules(Check):
def execute(self) -> list[CheckReportCloudflare]:
findings = []

for rule in firewall_client.rules:
report = CheckReportCloudflare(
metadata=self.metadata(),
resource=rule,
zone=rule.zone,
)

if rule.action in BLOCKING_ACTIONS:
report.status = "PASS"
report.status_extended = (
f"Firewall rule '{rule.name}' uses blocking action '{rule.action}'."
)
else:
report.status = "FAIL"
report.status_extended = f"Firewall rule '{rule.name}' uses non-blocking action '{rule.action}'."
findings.append(report)

return findings
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"Provider": "cloudflare",
"CheckID": "firewall_rate_limiting_configured",
"CheckTitle": "Firewall rule is configured as a rate limiting rule",
"CheckType": [],
"ServiceName": "firewall",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "informational",
"ResourceType": "FirewallRule",
"Description": "Identifies firewall rules configured as rate limiting rules to protect against volumetric attacks, brute force attempts, and API abuse.",
"Risk": "Without rate limiting rules, applications are vulnerable to DDoS attacks, credential brute forcing, and API abuse that can exhaust resources, compromise accounts, or cause service degradation.",
"RelatedUrl": "https://developers.cloudflare.com/waf/rate-limiting-rules/",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": "resource \"cloudflare_ruleset\" \"rate_limit\" {\n zone_id = var.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}"
},
"Recommendation": {
"Text": "Implement rate limiting rules as part of a defense in depth strategy. Configure appropriate thresholds based on expected traffic patterns to protect authentication endpoints, APIs, and resource-intensive operations.",
"Url": "https://hub.prowler.com/checks/firewall_rate_limiting_configured"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Rate limiting rules are in the http_ratelimit phase."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from prowler.lib.check.models import Check, CheckReportCloudflare
from prowler.providers.cloudflare.services.firewall.firewall_client import (
firewall_client,
)


class firewall_rate_limiting_configured(Check):
def execute(self) -> list[CheckReportCloudflare]:
findings = []

for rule in firewall_client.rules:
# Only evaluate rate limit phase rules
if rule.phase != "http_ratelimit":
continue

report = CheckReportCloudflare(
metadata=self.metadata(),
resource=rule,
zone=rule.zone,
)

if rule.enabled:
report.status = "PASS"
report.status_extended = f"Rate limiting rule '{rule.name}' is enabled."
else:
report.status = "FAIL"
report.status_extended = (
f"Rate limiting rule '{rule.name}' is disabled."
)
findings.append(report)

return findings
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Optional

from pydantic.v1 import BaseModel

from prowler.lib.logger import logger
from prowler.providers.cloudflare.lib.service.service import CloudflareService
from prowler.providers.cloudflare.models import CloudflareZone


class CloudflareFirewallRule(BaseModel):
"""Represents a firewall rule from custom rulesets."""

id: str
name: str = ""
description: Optional[str] = None
action: Optional[str] = None
enabled: bool = True
expression: Optional[str] = None
phase: Optional[str] = None
zone: CloudflareZone

class Config:
arbitrary_types_allowed = True


class Firewall(CloudflareService):
"""Collect Cloudflare firewall rules for each zone using rulesets API."""

def __init__(self, provider):
super().__init__(__class__.__name__, provider)
self.rules: list[CloudflareFirewallRule] = []
self.__threading_call__(self._list_firewall_rules)

def _list_firewall_rules(self, zone: CloudflareZone):
"""List firewall rules from custom rulesets for a zone."""
seen_ruleset_ids: set[str] = set()
try:
for ruleset in self.client.rulesets.list(zone_id=zone.id):
ruleset_id = getattr(ruleset, "id", "")
if ruleset_id in seen_ruleset_ids:
break
seen_ruleset_ids.add(ruleset_id)

ruleset_phase = getattr(ruleset, "phase", "")
if ruleset_phase in [
"http_request_firewall_custom",
"http_ratelimit",
"http_request_firewall_managed",
]:
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)
try:
self.rules.append(
CloudflareFirewallRule(
id=rule_id,
name=getattr(rule, "description", "")
or rule_id,
description=getattr(rule, "description", None),
action=getattr(rule, "action", None),
enabled=getattr(rule, "enabled", True),
expression=getattr(rule, "expression", None),
phase=ruleset_phase,
zone=zone,
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
Empty file.
4 changes: 4 additions & 0 deletions prowler/providers/cloudflare/services/waf/waf_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from prowler.providers.cloudflare.services.waf.waf_service import WAF
from prowler.providers.common.provider import Provider

waf_client = WAF(Provider.get_global_provider())
Loading