From 7bd13cdf391e0063a8381f1891dff43853c7467d Mon Sep 17 00:00:00 2001 From: "Andoni A." <14891798+andoniaf@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:08:33 +0100 Subject: [PATCH] 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. --- prowler/providers/image/lib/registry/base.py | 10 ++++++++-- .../lib/registry/test_dockerhub_adapter.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/prowler/providers/image/lib/registry/base.py b/prowler/providers/image/lib/registry/base.py index 6fb104d34a..5582f217bb 100644 --- a/prowler/providers/image/lib/registry/base.py +++ b/prowler/providers/image/lib/registry/base.py @@ -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__, diff --git a/tests/providers/image/lib/registry/test_dockerhub_adapter.py b/tests/providers/image/lib/registry/test_dockerhub_adapter.py index d27ca536ba..5334ea083e 100644 --- a/tests/providers/image/lib/registry/test_dockerhub_adapter.py +++ b/tests/providers/image/lib/registry/test_dockerhub_adapter.py @@ -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="Cloudflare error") + 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")