feat(cloudflare): --account-id filter support (#9894)

Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
Hugo Pereira Brito
2026-01-27 14:18:55 +01:00
committed by GitHub
parent 065827cd38
commit 80c94faff9
6 changed files with 144 additions and 4 deletions

View File

@@ -87,6 +87,30 @@ You can also use zone IDs instead of domain names:
prowler cloudflare -f 023e105f4ecef8ad9ca31a8372d0c353
```
## Filtering Accounts
By default, Prowler scans all accounts accessible with your credentials. If your API Token or API Key has access to multiple Cloudflare accounts, you can restrict the scan to specific accounts using the `--account-id` argument:
```bash
prowler cloudflare --account-id 372e67954025e0ba6aaa6d586b9e0b59
```
You can specify multiple account IDs:
```bash
prowler cloudflare --account-id 372e67954025e0ba6aaa6d586b9e0b59 9a7806061c88ada191ed06f989cc3dac
```
<Note>
If any of the provided account IDs are not found among the accounts accessible with your credentials, Prowler will raise an error and stop execution.
</Note>
You can combine account and zone filtering to narrow the scan scope further:
```bash
prowler cloudflare --account-id 372e67954025e0ba6aaa6d586b9e0b59 -f example.com
```
## Configuration
Prowler uses a configuration file to customize provider behavior. The Cloudflare configuration includes:

View File

@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840)
- `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828)
- CloudTrail Timeline abstraction for querying resource modification history [(#9101)](https://github.com/prowler-cloud/prowler/pull/9101)
- Cloudflare `--account-id` filter argument [(#9894)](https://github.com/prowler-cloud/prowler/pull/9894)
### Changed

View File

@@ -14,6 +14,7 @@ from prowler.lib.utils.utils import print_boxes
from prowler.providers.cloudflare.exceptions.exceptions import (
CloudflareCredentialsError,
CloudflareIdentityError,
CloudflareInvalidAccountError,
CloudflareSessionError,
)
from prowler.providers.cloudflare.lib.mutelist.mutelist import CloudflareMutelist
@@ -36,11 +37,13 @@ class CloudflareProvider(Provider):
_fixer_config: dict
_mutelist: CloudflareMutelist
_filter_zones: set[str] | None
_filter_accounts: set[str] | None
audit_metadata: Audit_Metadata
def __init__(
self,
filter_zones: Iterable[str] | None = None,
filter_accounts: Iterable[str] | None = None,
config_path: str = None,
config_content: dict | None = None,
fixer_config: dict = {},
@@ -74,6 +77,23 @@ class CloudflareProvider(Provider):
# Store zone filter for filtering resources across services
self._filter_zones = set(filter_zones) if filter_zones else None
# Store account filter and restrict audited_accounts accordingly
self._filter_accounts = set(filter_accounts) if filter_accounts else None
if self._filter_accounts:
discovered_account_ids = {account.id for account in self._identity.accounts}
invalid_accounts = self._filter_accounts - discovered_account_ids
if invalid_accounts:
invalid_str = ", ".join(sorted(invalid_accounts))
raise CloudflareInvalidAccountError(
file=os.path.basename(__file__),
message=f"Account IDs not found: {invalid_str}.",
)
self._identity.audited_accounts = [
account_id
for account_id in self._identity.audited_accounts
if account_id in self._filter_accounts
]
Provider.set_global_provider(self)
@property
@@ -105,6 +125,11 @@ class CloudflareProvider(Provider):
"""Zone filter from --region argument to filter resources."""
return self._filter_zones
@property
def filter_accounts(self) -> set[str] | None:
"""Account filter from --account-id argument to restrict scanned accounts."""
return self._filter_accounts
@property
def accounts(self) -> list[CloudflareAccount]:
return self._identity.accounts
@@ -248,10 +273,23 @@ class CloudflareProvider(Provider):
if email:
report_lines.append(f"Email: {Fore.YELLOW}{email}{Style.RESET_ALL}")
# Accounts
if self.accounts:
accounts = ", ".join([account.id for account in self.accounts])
report_lines.append(f"Accounts: {Fore.YELLOW}{accounts}{Style.RESET_ALL}")
# Audited accounts (only the ones that will actually be scanned)
audited_accounts = self.identity.audited_accounts
if audited_accounts:
account_names = {
account.id: account.name for account in self.identity.accounts
}
accounts_str = ", ".join(
(
f"{account_id} ({account_names[account_id]})"
if account_id in account_names and account_names[account_id]
else account_id
)
for account_id in audited_accounts
)
report_lines.append(
f"Audited Accounts: {Fore.YELLOW}{accounts_str}{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)

View File

@@ -5,6 +5,13 @@ def init_parser(self):
)
scope_group = cloudflare_parser.add_argument_group("Scope")
scope_group.add_argument(
"--account-id",
nargs="+",
default=None,
metavar="ACCOUNT_ID",
help="Filter scan to specific Cloudflare account IDs. Only zones belonging to these accounts will be scanned.",
)
scope_group.add_argument(
"--region",
"--filter-region",

View File

@@ -251,6 +251,7 @@ class Provider(ABC):
elif "cloudflare" in provider_class_name.lower():
provider_class(
filter_zones=arguments.region,
filter_accounts=arguments.account_id,
config_path=arguments.config_file,
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,

View File

@@ -5,6 +5,7 @@ import pytest
from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider
from prowler.providers.cloudflare.exceptions.exceptions import (
CloudflareCredentialsError,
CloudflareInvalidAccountError,
)
from prowler.providers.cloudflare.models import (
CloudflareAccount,
@@ -201,6 +202,74 @@ class TestCloudflareProvider:
assert provider.filter_zones == set(filter_zones)
def test_cloudflare_provider_with_filter_accounts(self):
with (
patch(
"prowler.providers.cloudflare.cloudflare_provider.CloudflareProvider.setup_session",
return_value=CloudflareSession(
client=MagicMock(),
api_token=API_TOKEN,
api_key=None,
api_email=None,
),
),
patch(
"prowler.providers.cloudflare.cloudflare_provider.CloudflareProvider.setup_identity",
return_value=CloudflareIdentityInfo(
user_id=USER_ID,
email=USER_EMAIL,
accounts=[
CloudflareAccount(
id=ACCOUNT_ID,
name=ACCOUNT_NAME,
type="standard",
),
CloudflareAccount(
id="other-account-id",
name="Other Account",
type="standard",
),
],
audited_accounts=[ACCOUNT_ID, "other-account-id"],
),
),
):
provider = CloudflareProvider(filter_accounts=[ACCOUNT_ID])
assert provider.filter_accounts == {ACCOUNT_ID}
# Only the filtered account should remain in audited_accounts
assert provider.identity.audited_accounts == [ACCOUNT_ID]
def test_cloudflare_provider_with_invalid_filter_accounts(self):
with (
patch(
"prowler.providers.cloudflare.cloudflare_provider.CloudflareProvider.setup_session",
return_value=CloudflareSession(
client=MagicMock(),
api_token=API_TOKEN,
api_key=None,
api_email=None,
),
),
patch(
"prowler.providers.cloudflare.cloudflare_provider.CloudflareProvider.setup_identity",
return_value=CloudflareIdentityInfo(
user_id=USER_ID,
email=USER_EMAIL,
accounts=[
CloudflareAccount(
id=ACCOUNT_ID,
name=ACCOUNT_NAME,
type="standard",
),
],
audited_accounts=[ACCOUNT_ID],
),
),
):
with pytest.raises(CloudflareInvalidAccountError):
CloudflareProvider(filter_accounts=["non-existent-account-id"])
def test_cloudflare_provider_properties(self):
with (
patch(