Skip to content

Commit 40dd0e6

Browse files
authored
fix(sdk): strip http(s):// scheme from image registry URLs (#10950)
1 parent 8db3a89 commit 40dd0e6

3 files changed

Lines changed: 84 additions & 6 deletions

File tree

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
2222

2323
- 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)
2424
- AWS `boto` user agent extra is now applied to every client [(#10944)](https://github.com/prowler-cloud/prowler/pull/10944)
25+
- 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)
2526

2627
### 🔐 Security
2728

prowler/providers/image/image_provider.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,12 +329,21 @@ def setup_session(self) -> None:
329329
"""Image provider doesn't need a session since it uses Trivy directly"""
330330
return None
331331

332+
@staticmethod
333+
def _strip_scheme(value: str) -> str:
334+
"""Remove a leading http:// or https:// scheme from a registry input."""
335+
for prefix in ("https://", "http://"):
336+
if value.lower().startswith(prefix):
337+
return value[len(prefix) :]
338+
return value
339+
332340
@staticmethod
333341
def _extract_registry(image: str) -> str | None:
334342
"""Extract registry hostname from an image reference.
335343
336344
Returns None for Docker Hub images (no registry prefix).
337345
"""
346+
image = ImageProvider._strip_scheme(image)
338347
parts = image.split("/")
339348
if len(parts) >= 2 and ("." in parts[0] or ":" in parts[0]):
340349
return parts[0]
@@ -348,6 +357,7 @@ def _is_registry_url(image_uid: str) -> bool:
348357
or "myregistry.com:5000" are registry URLs (dots in host, no slash).
349358
Image references like "alpine:3.18" or "nginx" are not.
350359
"""
360+
image_uid = ImageProvider._strip_scheme(image_uid)
351361
if "/" not in image_uid:
352362
host_part = image_uid.split(":")[0]
353363
if "." in host_part:
@@ -835,11 +845,9 @@ def _enumerate_registry(self) -> None:
835845
image_ref = f"{repo}:{tag}"
836846
else:
837847
# OCI registries need the full host/repo:tag reference
838-
registry_host = self.registry.rstrip("/")
839-
for prefix in ("https://", "http://"):
840-
if registry_host.startswith(prefix):
841-
registry_host = registry_host[len(prefix) :]
842-
break
848+
registry_host = ImageProvider._strip_scheme(
849+
self.registry.rstrip("/")
850+
)
843851
image_ref = f"{registry_host}/{repo}:{tag}"
844852
discovered_images.append(image_ref)
845853

@@ -977,6 +985,8 @@ def test_connection(
977985
if not image:
978986
return Connection(is_connected=False, error="Image name is required")
979987

988+
image = ImageProvider._strip_scheme(image)
989+
980990
# Registry URL (bare hostname) → test via OCI catalog
981991
if ImageProvider._is_registry_url(image):
982992
return ImageProvider._test_registry_connection(

tests/providers/image/image_provider_test.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
from prowler.lib.check.models import CheckReportImage
10+
from prowler.providers.common.provider import Provider
1011
from prowler.providers.image.exceptions.exceptions import (
1112
ImageInvalidConfigScannerError,
1213
ImageInvalidNameError,
@@ -20,7 +21,6 @@
2021
ImageScanError,
2122
ImageTrivyBinaryNotFoundError,
2223
)
23-
from prowler.providers.common.provider import Provider
2424
from prowler.providers.image.image_provider import ImageProvider
2525
from tests.providers.image.image_fixtures import (
2626
SAMPLE_IMAGE_SHA,
@@ -345,6 +345,24 @@ def test_test_connection_registry_url(self, mock_factory):
345345
)
346346
mock_adapter.list_repositories.assert_called_once()
347347

348+
@patch("prowler.providers.image.image_provider.create_registry_adapter")
349+
def test_test_connection_registry_url_with_https_scheme(self, mock_factory):
350+
"""Registry URL with https:// scheme is normalised before adapter creation."""
351+
mock_adapter = MagicMock()
352+
mock_adapter.list_repositories.return_value = ["repo1"]
353+
mock_factory.return_value = mock_adapter
354+
355+
result = ImageProvider.test_connection(image="https://my-registry.example.com")
356+
357+
assert result.is_connected is True
358+
mock_factory.assert_called_once_with(
359+
registry_url="my-registry.example.com",
360+
username=None,
361+
password=None,
362+
token=None,
363+
)
364+
mock_adapter.list_repositories.assert_called_once()
365+
348366
def test_build_status_extended(self):
349367
"""Test status message content for different finding types."""
350368
provider = _make_provider()
@@ -659,6 +677,27 @@ def test_print_credentials_shows_auth_method(self):
659677
assert "Docker login" in output
660678

661679

680+
class TestStripScheme:
681+
@pytest.mark.parametrize(
682+
"raw,expected",
683+
[
684+
("https://my-registry.example.com", "my-registry.example.com"),
685+
("http://my-registry.example.com", "my-registry.example.com"),
686+
("HTTPS://My-Registry.Example.Com", "My-Registry.Example.Com"),
687+
("Http://localhost:5000", "localhost:5000"),
688+
("my-registry.example.com", "my-registry.example.com"),
689+
("https://", ""),
690+
("https://https://nested.example.com", "https://nested.example.com"),
691+
(
692+
"ftp://not-a-supported-scheme.example.com",
693+
"ftp://not-a-supported-scheme.example.com",
694+
),
695+
],
696+
)
697+
def test_strip_scheme(self, raw, expected):
698+
assert ImageProvider._strip_scheme(raw) == expected
699+
700+
662701
class TestExtractRegistry:
663702
def test_docker_hub_simple(self):
664703
assert ImageProvider._extract_registry("alpine:3.18") is None
@@ -698,6 +737,24 @@ def test_digest_reference(self):
698737
def test_bare_image_name(self):
699738
assert ImageProvider._extract_registry("nginx") is None
700739

740+
def test_https_scheme_bare_hostname_returns_none(self):
741+
"""Bare scheme-prefixed hostname has no image path, so no registry is extracted."""
742+
assert (
743+
ImageProvider._extract_registry("https://my-registry.example.com") is None
744+
)
745+
746+
def test_http_scheme_with_port_stripped(self):
747+
assert (
748+
ImageProvider._extract_registry("http://localhost:5000/myimage:latest")
749+
== "localhost:5000"
750+
)
751+
752+
def test_https_scheme_with_path_stripped(self):
753+
assert (
754+
ImageProvider._extract_registry("https://ghcr.io/org/image:tag")
755+
== "ghcr.io"
756+
)
757+
701758

702759
class TestIsRegistryUrl:
703760
def test_bare_ecr_hostname(self):
@@ -728,6 +785,16 @@ def test_bare_image_no_tag(self):
728785
def test_dockerhub_namespace(self):
729786
assert not ImageProvider._is_registry_url("library/alpine")
730787

788+
def test_https_scheme_bare_hostname(self):
789+
assert ImageProvider._is_registry_url("https://my-registry.example.com")
790+
791+
def test_http_scheme_bare_hostname_with_port(self):
792+
assert ImageProvider._is_registry_url("http://my-registry.example.com:5000")
793+
794+
def test_https_scheme_image_reference_not_registry(self):
795+
"""A scheme-prefixed full image reference is still an image, not a registry URL."""
796+
assert not ImageProvider._is_registry_url("https://ghcr.io/myorg/repo:tag")
797+
731798

732799
class TestTestRegistryConnection:
733800
@patch("prowler.providers.image.image_provider.create_registry_adapter")

0 commit comments

Comments
 (0)