Compare commits

...
Author SHA1 Message Date
Hugo P.Brito 2e5d2053ee fix(sdk): address Cloudflare review feedback
- Clarify the missing challenge TTL changelog behavior
- Make account fallback expectations explicit in provider tests
2026-09-02 11:29:47 +01:00
Hugo P.Brito 83caa89d9f fix(cloudflare): handle missing challenge passage TTL
- Treat absent challenge TTL as non-compliant instead of raising
- Align credential fallback and status-message tests with current behavior
- Add the SDK changelog fragment
2026-09-01 11:23:22 +01:00
5 changed files with 37 additions and 20 deletions
@@ -0,0 +1 @@
Zones without a challenge passage TTL are reported as non-compliant by `zone_challenge_passage_configured`
@@ -29,17 +29,23 @@ class zone_challenge_passage_configured(Check):
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:
challenge_ttl = zone.settings.challenge_ttl
if challenge_ttl is None:
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)."
f"Challenge Passage is not configured for zone {zone.name}."
)
else:
# API returns seconds, convert to minutes
challenge_ttl_minutes = 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
@@ -9,7 +9,6 @@ from prowler.providers.cloudflare.exceptions.exceptions import (
CloudflareInvalidAPIKeyError,
CloudflareInvalidAPITokenError,
CloudflareNoAccountsError,
CloudflareUserTokenRequiredError,
)
from prowler.providers.cloudflare.models import (
CloudflareAccount,
@@ -308,8 +307,8 @@ class TestCloudflareValidateCredentials:
CloudflareProvider.validate_credentials(session)
mock_client.user.get.assert_called_once()
def test_validate_credentials_user_token_required(self):
"""Test that user token required error is raised for Account tokens."""
def test_validate_credentials_account_token_without_accounts(self):
"""Test that account tokens fall back to account discovery."""
mock_client = MagicMock()
# Simulate error code 9109 - user-level authentication required
from cloudflare._exceptions import PermissionDeniedError
@@ -319,6 +318,7 @@ class TestCloudflareValidateCredentials:
response=MagicMock(status_code=403),
body=None,
)
mock_client.accounts.list.return_value = iter([])
session = CloudflareSession(
client=mock_client,
@@ -327,8 +327,10 @@ class TestCloudflareValidateCredentials:
api_email=None,
)
with pytest.raises(CloudflareUserTokenRequiredError):
with pytest.raises(CloudflareNoAccountsError):
CloudflareProvider.validate_credentials(session)
mock_client.user.get.assert_called_once()
mock_client.accounts.list.assert_called_once()
def test_validate_credentials_invalid_api_token(self):
"""Test that invalid API token error is raised."""
@@ -487,8 +489,8 @@ class TestCloudflareTestConnection:
assert connection.is_connected is False
assert isinstance(connection.error, CloudflareInvalidAPITokenError)
def test_test_connection_user_token_required(self):
"""Test that user token required error is properly returned."""
def test_test_connection_account_token_without_accounts(self):
"""Test that account tokens without accessible accounts return an error."""
mock_client = MagicMock()
from cloudflare._exceptions import PermissionDeniedError
@@ -497,6 +499,7 @@ class TestCloudflareTestConnection:
response=MagicMock(status_code=403),
body=None,
)
mock_client.accounts.list.return_value = iter([])
with patch(
"prowler.providers.cloudflare.cloudflare_provider.CloudflareProvider.setup_session",
@@ -512,9 +515,10 @@ class TestCloudflareTestConnection:
)
assert connection.is_connected is False
assert isinstance(connection.error, CloudflareUserTokenRequiredError)
# Verify the error message is user-friendly
assert "User-level API token required" in str(connection.error)
assert isinstance(connection.error, CloudflareNoAccountsError)
assert "No Cloudflare accounts found" in str(connection.error)
mock_client.user.get.assert_called_once()
mock_client.accounts.list.assert_called_once()
def test_test_connection_invalid_api_key(self):
"""Test that invalid API key error is properly returned."""
@@ -240,3 +240,7 @@ class Test_zone_challenge_passage_configured:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Challenge Passage is not configured for zone {ZONE_NAME}."
)
@@ -102,8 +102,10 @@ class Test_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
assert (
result[0].status_extended
== f"Development mode is enabled for zone {ZONE_NAME}."
)
def test_zone_development_mode_none(self):
zone_client = mock.MagicMock