fix(sdk): strip http(s):// scheme from image registry URLs (#10950)

This commit is contained in:
Andoni Alonso
2026-05-04 08:37:46 +02:00
committed by GitHub
parent 8db3a89669
commit 40dd0e640b
3 changed files with 84 additions and 6 deletions
+1
View File
@@ -22,6 +22,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- AWS SDK test isolation: autouse `mock_aws` fixture and leak detector in `conftest.py` to prevent tests from hitting real AWS endpoints, with idempotent organization setup for tests calling `set_mocked_aws_provider` multiple times [(#10605)](https://github.com/prowler-cloud/prowler/pull/10605)
- AWS `boto` user agent extra is now applied to every client [(#10944)](https://github.com/prowler-cloud/prowler/pull/10944)
- Image provider connection check no longer fails with a misleading `host='https'` resolution error when the registry URL includes an `http://` or `https://` scheme prefix [(#10950)](https://github.com/prowler-cloud/prowler/pull/10950)
### 🔐 Security
+15 -5
View File
@@ -329,12 +329,21 @@ class ImageProvider(Provider):
"""Image provider doesn't need a session since it uses Trivy directly"""
return None
@staticmethod
def _strip_scheme(value: str) -> str:
"""Remove a leading http:// or https:// scheme from a registry input."""
for prefix in ("https://", "http://"):
if value.lower().startswith(prefix):
return value[len(prefix) :]
return value
@staticmethod
def _extract_registry(image: str) -> str | None:
"""Extract registry hostname from an image reference.
Returns None for Docker Hub images (no registry prefix).
"""
image = ImageProvider._strip_scheme(image)
parts = image.split("/")
if len(parts) >= 2 and ("." in parts[0] or ":" in parts[0]):
return parts[0]
@@ -348,6 +357,7 @@ class ImageProvider(Provider):
or "myregistry.com:5000" are registry URLs (dots in host, no slash).
Image references like "alpine:3.18" or "nginx" are not.
"""
image_uid = ImageProvider._strip_scheme(image_uid)
if "/" not in image_uid:
host_part = image_uid.split(":")[0]
if "." in host_part:
@@ -835,11 +845,9 @@ class ImageProvider(Provider):
image_ref = f"{repo}:{tag}"
else:
# OCI registries need the full host/repo:tag reference
registry_host = self.registry.rstrip("/")
for prefix in ("https://", "http://"):
if registry_host.startswith(prefix):
registry_host = registry_host[len(prefix) :]
break
registry_host = ImageProvider._strip_scheme(
self.registry.rstrip("/")
)
image_ref = f"{registry_host}/{repo}:{tag}"
discovered_images.append(image_ref)
@@ -977,6 +985,8 @@ class ImageProvider(Provider):
if not image:
return Connection(is_connected=False, error="Image name is required")
image = ImageProvider._strip_scheme(image)
# Registry URL (bare hostname) → test via OCI catalog
if ImageProvider._is_registry_url(image):
return ImageProvider._test_registry_connection(
+68 -1
View File
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
import pytest
from prowler.lib.check.models import CheckReportImage
from prowler.providers.common.provider import Provider
from prowler.providers.image.exceptions.exceptions import (
ImageInvalidConfigScannerError,
ImageInvalidNameError,
@@ -20,7 +21,6 @@ from prowler.providers.image.exceptions.exceptions import (
ImageScanError,
ImageTrivyBinaryNotFoundError,
)
from prowler.providers.common.provider import Provider
from prowler.providers.image.image_provider import ImageProvider
from tests.providers.image.image_fixtures import (
SAMPLE_IMAGE_SHA,
@@ -345,6 +345,24 @@ class TestImageProvider:
)
mock_adapter.list_repositories.assert_called_once()
@patch("prowler.providers.image.image_provider.create_registry_adapter")
def test_test_connection_registry_url_with_https_scheme(self, mock_factory):
"""Registry URL with https:// scheme is normalised before adapter creation."""
mock_adapter = MagicMock()
mock_adapter.list_repositories.return_value = ["repo1"]
mock_factory.return_value = mock_adapter
result = ImageProvider.test_connection(image="https://my-registry.example.com")
assert result.is_connected is True
mock_factory.assert_called_once_with(
registry_url="my-registry.example.com",
username=None,
password=None,
token=None,
)
mock_adapter.list_repositories.assert_called_once()
def test_build_status_extended(self):
"""Test status message content for different finding types."""
provider = _make_provider()
@@ -659,6 +677,27 @@ class TestImageProviderRegistryAuth:
assert "Docker login" in output
class TestStripScheme:
@pytest.mark.parametrize(
"raw,expected",
[
("https://my-registry.example.com", "my-registry.example.com"),
("http://my-registry.example.com", "my-registry.example.com"),
("HTTPS://My-Registry.Example.Com", "My-Registry.Example.Com"),
("Http://localhost:5000", "localhost:5000"),
("my-registry.example.com", "my-registry.example.com"),
("https://", ""),
("https://https://nested.example.com", "https://nested.example.com"),
(
"ftp://not-a-supported-scheme.example.com",
"ftp://not-a-supported-scheme.example.com",
),
],
)
def test_strip_scheme(self, raw, expected):
assert ImageProvider._strip_scheme(raw) == expected
class TestExtractRegistry:
def test_docker_hub_simple(self):
assert ImageProvider._extract_registry("alpine:3.18") is None
@@ -698,6 +737,24 @@ class TestExtractRegistry:
def test_bare_image_name(self):
assert ImageProvider._extract_registry("nginx") is None
def test_https_scheme_bare_hostname_returns_none(self):
"""Bare scheme-prefixed hostname has no image path, so no registry is extracted."""
assert (
ImageProvider._extract_registry("https://my-registry.example.com") is None
)
def test_http_scheme_with_port_stripped(self):
assert (
ImageProvider._extract_registry("http://localhost:5000/myimage:latest")
== "localhost:5000"
)
def test_https_scheme_with_path_stripped(self):
assert (
ImageProvider._extract_registry("https://ghcr.io/org/image:tag")
== "ghcr.io"
)
class TestIsRegistryUrl:
def test_bare_ecr_hostname(self):
@@ -728,6 +785,16 @@ class TestIsRegistryUrl:
def test_dockerhub_namespace(self):
assert not ImageProvider._is_registry_url("library/alpine")
def test_https_scheme_bare_hostname(self):
assert ImageProvider._is_registry_url("https://my-registry.example.com")
def test_http_scheme_bare_hostname_with_port(self):
assert ImageProvider._is_registry_url("http://my-registry.example.com:5000")
def test_https_scheme_image_reference_not_registry(self):
"""A scheme-prefixed full image reference is still an image, not a registry URL."""
assert not ImageProvider._is_registry_url("https://ghcr.io/myorg/repo:tag")
class TestTestRegistryConnection:
@patch("prowler.providers.image.image_provider.create_registry_adapter")