mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-06 08:47:18 +00:00
fix(sdk): strip http(s):// scheme from image registry URLs (#10950)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user