fix(image): set custom User-Agent to avoid Cloudflare bot blocking

Cloudflare bot management on Docker Hub blocks the default
python-requests User-Agent from container IPs, returning HTTP 500.
Add a Prowler/1.0 User-Agent header to all registry requests and
include truncated response body in 5xx retry logs for diagnostics.
This commit is contained in:
Andoni A.
2026-02-18 11:08:33 +01:00
parent 34cbacc105
commit 7bd13cdf39
2 changed files with 25 additions and 2 deletions
+8 -2
View File
@@ -13,6 +13,7 @@ from prowler.providers.image.exceptions.exceptions import ImageRegistryNetworkEr
_MAX_RETRIES = 3
_BACKOFF_BASE = 1
_USER_AGENT = "Prowler/1.0 (registry-adapter)"
class RegistryAdapter(ABC):
@@ -69,8 +70,12 @@ class RegistryAdapter(ABC):
context_label = kwargs.pop("context_label", None) or self.registry_url
kwargs.setdefault("timeout", 30)
kwargs.setdefault("verify", self.verify_ssl)
headers = kwargs.get("headers", {})
headers.setdefault("User-Agent", _USER_AGENT)
kwargs["headers"] = headers
last_exception = None
last_status = None
last_body = None
for attempt in range(1, _MAX_RETRIES + 1):
try:
resp = requests.request(method, url, **kwargs)
@@ -84,10 +89,11 @@ class RegistryAdapter(ABC):
continue
if resp.status_code >= 500:
last_status = resp.status_code
last_body = (resp.text or "")[:500]
wait = _BACKOFF_BASE * (2 ** (attempt - 1))
logger.warning(
f"Server error from {context_label} (HTTP {resp.status_code}), "
f"retrying in {wait}s (attempt {attempt}/{_MAX_RETRIES})"
f"retrying in {wait}s (attempt {attempt}/{_MAX_RETRIES}): {last_body}"
)
time.sleep(wait)
continue
@@ -115,7 +121,7 @@ class RegistryAdapter(ABC):
if last_status is not None and last_status >= 500:
raise ImageRegistryNetworkError(
file=__file__,
message=f"Server error from {context_label} (HTTP {last_status}) after {_MAX_RETRIES} attempts.",
message=f"Server error from {context_label} (HTTP {last_status}) after {_MAX_RETRIES} attempts: {last_body}",
)
raise ImageRegistryNetworkError(
file=__file__,
@@ -191,6 +191,23 @@ class TestDockerHubRetry:
assert mock_request.call_count == 1
mock_sleep.assert_not_called()
@patch("prowler.providers.image.lib.registry.base.requests.request")
def test_request_sends_user_agent(self, mock_request):
mock_request.return_value = MagicMock(status_code=200)
adapter = DockerHubAdapter("docker.io/myorg")
adapter._request_with_retry("GET", "https://hub.docker.com")
_, kwargs = mock_request.call_args
assert kwargs["headers"]["User-Agent"] == "Prowler/1.0 (registry-adapter)"
@patch("prowler.providers.image.lib.registry.base.time.sleep")
@patch("prowler.providers.image.lib.registry.base.requests.request")
def test_retry_500_includes_response_body(self, mock_request, mock_sleep):
resp_500 = MagicMock(status_code=500, text="<html>Cloudflare error</html>")
mock_request.return_value = resp_500
adapter = DockerHubAdapter("docker.io/myorg")
with pytest.raises(ImageRegistryNetworkError, match="Cloudflare error"):
adapter._request_with_retry("GET", "https://hub.docker.com")
class TestDockerHubEmptyTokens:
@patch("prowler.providers.image.lib.registry.base.requests.request")