feat(api): add Cloudflare provider support (#9907)

This commit is contained in:
Hugo Pereira Brito authored and GitHub committed 2026-02-02 14:08:33 +01:00
1 parent d01f399cb2
commit b1f9971617
13 files changed
+508 -68

No files matched your search

@@ -0,0 +1,38 @@
# Generated by Django migration for Cloudflare provider support
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0074_findings_fail_new_index_parent"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("mongodbatlas", "MongoDB Atlas"),
("iac", "IaC"),
("oraclecloud", "Oracle Cloud Infrastructure"),
("alibabacloud", "Alibaba Cloud"),
("cloudflare", "Cloudflare"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'cloudflare';",
reverse_sql=migrations.RunSQL.noop,
),
]
+10
View File
@@ -287,6 +287,7 @@ class Provider(RowLevelSecurityProtectedModel):
IAC = "iac", _("IaC")
ORACLECLOUD = "oraclecloud", _("Oracle Cloud Infrastructure")
ALIBABACLOUD = "alibabacloud", _("Alibaba Cloud")
CLOUDFLARE = "cloudflare", _("Cloudflare")
@staticmethod
def validate_aws_uid(value):
@@ -400,6 +401,15 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_cloudflare_uid(value):
if not re.match(r"^[a-f0-9]{32}$", value):
raise ModelValidationError(
detail="Cloudflare Account ID must be a 32-character hexadecimal string.",
code="cloudflare-uid",
pointer="/data/attributes/uid",
)
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
File diff suppressed because it is too large. Load diff
+6
View File
@@ -20,6 +20,7 @@ from prowler.providers.alibabacloud.alibabacloud_provider import AlibabacloudPro
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.iac.iac_provider import IacProvider
@@ -118,6 +119,7 @@ class TestReturnProwlerProvider:
(Provider.ProviderChoices.ORACLECLOUD.value, OraclecloudProvider),
(Provider.ProviderChoices.IAC.value, IacProvider),
(Provider.ProviderChoices.ALIBABACLOUD.value, AlibabacloudProvider),
(Provider.ProviderChoices.CLOUDFLARE.value, CloudflareProvider),
],
)
def test_return_prowler_provider(self, provider_type, expected_provider):
@@ -221,6 +223,10 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.MONGODBATLAS.value,
{"atlas_organization_id": "provider_uid"},
),
(
Provider.ProviderChoices.CLOUDFLARE.value,
{"filter_accounts": ["provider_uid"]},
),
],
)
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
+66 -4
View File
@@ -1174,6 +1174,11 @@ class TestProviderViewSet:
"uid": "1234567890123456",
"alias": "Alibaba Cloud Account",
},
{
"provider": "cloudflare",
"uid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"alias": "Cloudflare Account",
},
]
),
)
@@ -1553,6 +1558,46 @@ class TestProviderViewSet:
"alibabacloud-uid",
"uid",
),
# Cloudflare UID validation - too short (not 32 hex chars)
(
{
"provider": "cloudflare",
"uid": "abc123",
"alias": "test",
},
"cloudflare-uid",
"uid",
),
# Cloudflare UID validation - uppercase hex (must be lowercase)
(
{
"provider": "cloudflare",
"uid": "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4",
"alias": "test",
},
"cloudflare-uid",
"uid",
),
# Cloudflare UID validation - non-hex characters
(
{
"provider": "cloudflare",
"uid": "g1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"alias": "test",
},
"cloudflare-uid",
"uid",
),
# Cloudflare UID validation - too long (33 chars)
(
{
"provider": "cloudflare",
"uid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e",
"alias": "test",
},
"cloudflare-uid",
"uid",
),
]
),
)
@@ -1726,21 +1771,21 @@ class TestProviderViewSet:
(
"uid.icontains",
"1",
8,
9,
),
("alias", "aws_testing_1", 1),
("alias.icontains", "aws", 2),
("inserted_at", TODAY, 9),
("inserted_at", TODAY, 10),
(
"inserted_at.gte",
"2024-01-01",
9,
10,
),
("inserted_at.lte", "2024-01-01", 0),
(
"updated_at.gte",
"2024-01-01",
9,
10,
),
("updated_at.lte", "2024-01-01", 0),
]
@@ -2330,6 +2375,23 @@ class TestProviderSecretViewSet:
"role_session_name": "ProwlerAuditSession",
},
),
# Cloudflare with API Token
(
Provider.ProviderChoices.CLOUDFLARE.value,
ProviderSecret.TypeChoices.STATIC,
{
"api_token": "fake-cloudflare-api-token-for-testing",
},
),
# Cloudflare with API Key + Email
(
Provider.ProviderChoices.CLOUDFLARE.value,
ProviderSecret.TypeChoices.STATIC,
{
"api_key": "fake-cloudflare-api-key-for-testing",
"api_email": "user@example.com",
},
),
],
)
def test_provider_secrets_create_valid(
+15 -2
View File
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
)
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.iac.iac_provider import IacProvider
@@ -91,7 +92,7 @@ def return_prowler_provider(
provider (Provider): The provider object containing the provider type and associated secrets.
Returns:
AlibabacloudProvider | AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: The corresponding provider class.
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: The corresponding provider class.
Raises:
ValueError: If the provider type specified in `provider.provider` is not supported.
@@ -145,6 +146,12 @@ def return_prowler_provider(
)
prowler_provider = AlibabacloudProvider
case Provider.ProviderChoices.CLOUDFLARE.value:
from prowler.providers.cloudflare.cloudflare_provider import (
CloudflareProvider,
)
prowler_provider = CloudflareProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
return prowler_provider
@@ -196,6 +203,11 @@ def get_prowler_provider_kwargs(
**prowler_provider_kwargs,
"atlas_organization_id": provider.uid,
}
elif provider.provider == Provider.ProviderChoices.CLOUDFLARE.value:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"filter_accounts": [provider.uid],
}
if mutelist_processor:
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
@@ -213,6 +225,7 @@ def initialize_prowler_provider(
AlibabacloudProvider
| AwsProvider
| AzureProvider
| CloudflareProvider
| GcpProvider
| GithubProvider
| IacProvider
@@ -228,7 +241,7 @@ def initialize_prowler_provider(
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
AlibabacloudProvider | AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: An instance of the corresponding provider class
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: An instance of the corresponding provider class
initialized with the provider's secrets.
"""
prowler_provider = return_prowler_provider(provider)
@@ -346,6 +346,33 @@ from rest_framework_json_api import serializers
},
"required": ["role_arn", "access_key_id", "access_key_secret"],
},
{
"type": "object",
"title": "Cloudflare API Token",
"properties": {
"api_token": {
"type": "string",
"description": "Cloudflare API Token for authentication (recommended).",
},
},
"required": ["api_token"],
},
{
"type": "object",
"title": "Cloudflare API Key + Email",
"properties": {
"api_key": {
"type": "string",
"description": "Cloudflare Global API Key for authentication (legacy).",
},
"api_email": {
"type": "string",
"format": "email",
"description": "Email address associated with the Cloudflare account.",
},
},
"required": ["api_key", "api_email"],
},
]
}
)
+27
View File
@@ -1503,6 +1503,18 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = MongoDBAtlasProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.ALIBABACLOUD.value:
serializer = AlibabaCloudProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.CLOUDFLARE.value:
if "api_token" in secret:
serializer = CloudflareTokenProviderSecret(data=secret)
elif "api_key" in secret and "api_email" in secret:
serializer = CloudflareApiKeyProviderSecret(data=secret)
else:
raise serializers.ValidationError(
{
"secret": "Cloudflare credentials must include either 'api_token' "
"or both 'api_key' and 'api_email'."
}
)
else:
raise serializers.ValidationError(
{"provider": f"Provider type not supported {provider_type}"}
@@ -1654,6 +1666,21 @@ class OracleCloudProviderSecret(serializers.Serializer):
resource_name = "provider-secrets"
class CloudflareTokenProviderSecret(serializers.Serializer):
api_token = serializers.CharField()
class Meta:
resource_name = "provider-secrets"
class CloudflareApiKeyProviderSecret(serializers.Serializer):
api_key = serializers.CharField()
api_email = serializers.EmailField()
class Meta:
resource_name = "provider-secrets"
class AlibabaCloudProviderSecret(serializers.Serializer):
access_key_id = serializers.CharField()
access_key_secret = serializers.CharField()
+13 -8
View File
@@ -1,11 +1,9 @@
import logging
from types import SimpleNamespace
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from allauth.socialaccount.models import SocialLogin
from django.conf import settings
from django.db import connection as django_connection
@@ -14,6 +12,11 @@ from django.urls import reverse
from django_celery_results.models import TaskResult
from rest_framework import status
from rest_framework.test import APIClient
from tasks.jobs.backfill import (
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
backfill_scan_resource_group_summaries,
)
from api.attack_paths import (
AttackPathsQueryDefinition,
@@ -59,11 +62,6 @@ from api.rls import Tenant
from api.v1.serializers import TokenSerializer
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
from tasks.jobs.backfill import (
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
backfill_scan_resource_group_summaries,
)
TODAY = str(datetime.today().date())
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
@@ -533,6 +531,12 @@ def providers_fixture(tenants_fixture):
alias="alibabacloud_testing",
tenant_id=tenant.id,
)
provider10 = Provider.objects.create(
provider="cloudflare",
uid="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
alias="cloudflare_testing",
tenant_id=tenant.id,
)
return (
provider1,
@@ -544,6 +548,7 @@ def providers_fixture(tenants_fixture):
provider7,
provider8,
provider9,
provider10,
)