mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-20 11:01:52 +00:00
fix(image): harden registry adapter layer
- Fix shared dict mutation in ImageBaseException (thread-safety) - Handle unexpected status codes in OCI adapter auth flow - Force TLS verification for Docker Hub endpoints - Add credential-safe __repr__ to RegistryAdapter - Rename _next_page_url override to _next_tag_page_url in DockerHubAdapter - Distinguish rate-limit from connection errors in retry logic - Add type hints to all public adapter methods - Remove unused imports in test files (flake8)
This commit is contained in:
@@ -82,8 +82,8 @@ class ImageBaseException(ProwlerException):
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
error_info = self.IMAGE_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
if error_info and message:
|
||||
error_info = {**error_info, "message": message}
|
||||
super().__init__(
|
||||
code,
|
||||
source="Image",
|
||||
|
||||
@@ -9,9 +9,7 @@ from abc import ABC, abstractmethod
|
||||
import requests
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.image.exceptions.exceptions import (
|
||||
ImageRegistryNetworkError,
|
||||
)
|
||||
from prowler.providers.image.exceptions.exceptions import ImageRegistryNetworkError
|
||||
|
||||
_MAX_RETRIES = 3
|
||||
_BACKOFF_BASE = 1
|
||||
@@ -21,14 +19,28 @@ class RegistryAdapter(ABC):
|
||||
"""Abstract base class for registry adapters."""
|
||||
|
||||
def __init__(
|
||||
self, registry_url, username=None, password=None, token=None, verify_ssl=True
|
||||
):
|
||||
self,
|
||||
registry_url: str,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
verify_ssl: bool = True,
|
||||
) -> None:
|
||||
self.registry_url = registry_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.token = token
|
||||
self.verify_ssl = verify_ssl
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__class__.__name__}("
|
||||
f"registry_url={self.registry_url!r}, "
|
||||
f"username={self.username!r}, "
|
||||
f"password={'<redacted>' if self.password else None}, "
|
||||
f"token={'<redacted>' if self.token else None})"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def list_repositories(self) -> list[str]:
|
||||
"""Enumerate all repository names in the registry."""
|
||||
@@ -39,15 +51,17 @@ class RegistryAdapter(ABC):
|
||||
"""Enumerate all tags for a repository."""
|
||||
...
|
||||
|
||||
def _request_with_retry(self, method, url, **kwargs):
|
||||
def _request_with_retry(self, method: str, url: str, **kwargs) -> requests.Response:
|
||||
context_label = kwargs.pop("context_label", None) or self.registry_url
|
||||
kwargs.setdefault("timeout", 30)
|
||||
kwargs.setdefault("verify", self.verify_ssl)
|
||||
last_exception = None
|
||||
last_status = None
|
||||
for attempt in range(1, _MAX_RETRIES + 1):
|
||||
try:
|
||||
resp = requests.request(method, url, **kwargs)
|
||||
if resp.status_code == 429:
|
||||
last_status = 429
|
||||
wait = _BACKOFF_BASE * (2 ** (attempt - 1))
|
||||
logger.warning(
|
||||
f"Rate limited by {context_label}, retrying in {wait}s (attempt {attempt}/{_MAX_RETRIES})"
|
||||
@@ -70,6 +84,11 @@ class RegistryAdapter(ABC):
|
||||
message=f"Connection timed out to {context_label}.",
|
||||
original_exception=exc,
|
||||
)
|
||||
if last_status == 429:
|
||||
raise ImageRegistryNetworkError(
|
||||
file=__file__,
|
||||
message=f"Rate limited by {context_label} after {_MAX_RETRIES} attempts.",
|
||||
)
|
||||
raise ImageRegistryNetworkError(
|
||||
file=__file__,
|
||||
message=f"Failed to connect to {context_label} after {_MAX_RETRIES} attempts.",
|
||||
@@ -77,7 +96,7 @@ class RegistryAdapter(ABC):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _next_page_url(resp):
|
||||
def _next_page_url(resp: requests.Response) -> str | None:
|
||||
link_header = resp.headers.get("Link", "")
|
||||
if not link_header:
|
||||
return None
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.image.exceptions.exceptions import (
|
||||
ImageRegistryAuthError,
|
||||
@@ -21,15 +23,24 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
"""Adapter for Docker Hub using the Hub REST API + OCI tag listing."""
|
||||
|
||||
def __init__(
|
||||
self, registry_url, username=None, password=None, token=None, verify_ssl=True
|
||||
):
|
||||
super().__init__(registry_url, username, password, token, verify_ssl)
|
||||
self,
|
||||
registry_url: str,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
verify_ssl: bool = True,
|
||||
) -> None:
|
||||
if not verify_ssl:
|
||||
logger.warning(
|
||||
"Docker Hub always uses TLS verification; --registry-insecure is ignored for Docker Hub registries."
|
||||
)
|
||||
super().__init__(registry_url, username, password, token, verify_ssl=True)
|
||||
self.namespace = self._extract_namespace(registry_url)
|
||||
self._hub_jwt = None
|
||||
self._registry_tokens = {}
|
||||
self._hub_jwt: str | None = None
|
||||
self._registry_tokens: dict[str, str] = {}
|
||||
|
||||
@staticmethod
|
||||
def _extract_namespace(registry_url):
|
||||
def _extract_namespace(registry_url: str) -> str:
|
||||
url = registry_url.rstrip("/")
|
||||
for prefix in (
|
||||
"https://registry-1.docker.io",
|
||||
@@ -49,19 +60,19 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
namespace = parts[0] if parts and parts[0] else ""
|
||||
return namespace
|
||||
|
||||
def list_repositories(self):
|
||||
def list_repositories(self) -> list[str]:
|
||||
if not self.namespace:
|
||||
raise ImageRegistryAuthError(
|
||||
file=__file__,
|
||||
message="Docker Hub requires a namespace. Use --registry docker.io/{org_or_user}.",
|
||||
)
|
||||
self._hub_login()
|
||||
repositories = []
|
||||
repositories: list[str] = []
|
||||
if self._hub_jwt:
|
||||
url = f"{_HUB_API}/v2/namespaces/{self.namespace}/repositories"
|
||||
else:
|
||||
url = f"{_HUB_API}/v2/repositories/{self.namespace}/"
|
||||
params = {"page_size": 100}
|
||||
params: dict = {"page_size": 100}
|
||||
while url:
|
||||
resp = self._hub_request("GET", url, params=params)
|
||||
self._check_hub_response(resp, "repository listing")
|
||||
@@ -74,11 +85,11 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
params = {}
|
||||
return repositories
|
||||
|
||||
def list_tags(self, repository):
|
||||
def list_tags(self, repository: str) -> list[str]:
|
||||
token = self._get_registry_token(repository)
|
||||
tags = []
|
||||
tags: list[str] = []
|
||||
url = f"{_REGISTRY_HOST}/v2/{repository}/tags/list"
|
||||
params = {"n": 100}
|
||||
params: dict = {"n": 100}
|
||||
while url:
|
||||
resp = self._registry_request("GET", url, token, params=params)
|
||||
if resp.status_code in (401, 403):
|
||||
@@ -93,11 +104,11 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
break
|
||||
data = resp.json()
|
||||
tags.extend(data.get("tags", []) or [])
|
||||
url = self._next_page_url(resp)
|
||||
url = self._next_tag_page_url(resp)
|
||||
params = {}
|
||||
return tags
|
||||
|
||||
def _hub_login(self):
|
||||
def _hub_login(self) -> None:
|
||||
if self._hub_jwt:
|
||||
return
|
||||
if not self.username or not self.password:
|
||||
@@ -115,7 +126,7 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
)
|
||||
self._hub_jwt = resp.json().get("token")
|
||||
|
||||
def _get_registry_token(self, repository):
|
||||
def _get_registry_token(self, repository: str) -> str:
|
||||
if repository in self._registry_tokens:
|
||||
return self._registry_tokens[repository]
|
||||
params = {
|
||||
@@ -126,7 +137,11 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
if self.username and self.password:
|
||||
auth = (self.username, self.password)
|
||||
resp = self._request_with_retry(
|
||||
"GET", _AUTH_URL, params=params, auth=auth, context_label="Docker Hub",
|
||||
"GET",
|
||||
_AUTH_URL,
|
||||
params=params,
|
||||
auth=auth,
|
||||
context_label="Docker Hub",
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise ImageRegistryAuthError(
|
||||
@@ -137,7 +152,7 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
self._registry_tokens[repository] = token
|
||||
return token
|
||||
|
||||
def _hub_request(self, method, url, **kwargs):
|
||||
def _hub_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
||||
headers = kwargs.pop("headers", {})
|
||||
if self._hub_jwt:
|
||||
headers["Authorization"] = f"Bearer {self._hub_jwt}"
|
||||
@@ -146,7 +161,9 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
method, url, context_label="Docker Hub", **kwargs
|
||||
)
|
||||
|
||||
def _registry_request(self, method, url, token, **kwargs):
|
||||
def _registry_request(
|
||||
self, method: str, url: str, token: str, **kwargs
|
||||
) -> requests.Response:
|
||||
headers = kwargs.pop("headers", {})
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
kwargs["headers"] = headers
|
||||
@@ -154,7 +171,7 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
method, url, context_label="Docker Hub", **kwargs
|
||||
)
|
||||
|
||||
def _check_hub_response(self, resp, context):
|
||||
def _check_hub_response(self, resp: requests.Response, context: str) -> None:
|
||||
if resp.status_code == 200:
|
||||
return
|
||||
if resp.status_code in (401, 403):
|
||||
@@ -173,7 +190,7 @@ class DockerHubAdapter(RegistryAdapter):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _next_page_url(resp):
|
||||
def _next_tag_page_url(resp: requests.Response) -> str | None:
|
||||
link_header = resp.headers.get("Link", "")
|
||||
if not link_header:
|
||||
return None
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from prowler.providers.image.lib.registry.base import RegistryAdapter
|
||||
from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter
|
||||
from prowler.providers.image.lib.registry.oci_adapter import OciRegistryAdapter
|
||||
|
||||
@@ -16,8 +17,12 @@ _ECR_PATTERN = re.compile(
|
||||
|
||||
|
||||
def create_registry_adapter(
|
||||
registry_url, username=None, password=None, token=None, verify_ssl=True
|
||||
):
|
||||
registry_url: str,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
verify_ssl: bool = True,
|
||||
) -> RegistryAdapter:
|
||||
"""Auto-detect registry type from URL and return the appropriate adapter."""
|
||||
if _DOCKER_HUB_PATTERN.search(registry_url):
|
||||
return DockerHubAdapter(
|
||||
@@ -38,7 +43,7 @@ def create_registry_adapter(
|
||||
)
|
||||
|
||||
|
||||
def detect_registry_type(registry_url):
|
||||
def detect_registry_type(registry_url: str) -> str:
|
||||
"""Return a string identifying the detected registry type."""
|
||||
if _DOCKER_HUB_PATTERN.search(registry_url):
|
||||
return "dockerhub"
|
||||
|
||||
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
import base64
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from prowler.providers.image.exceptions.exceptions import (
|
||||
ImageRegistryAuthError,
|
||||
ImageRegistryCatalogError,
|
||||
@@ -17,25 +19,30 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
"""Adapter for registries implementing OCI Distribution Spec."""
|
||||
|
||||
def __init__(
|
||||
self, registry_url, username=None, password=None, token=None, verify_ssl=True
|
||||
):
|
||||
self,
|
||||
registry_url: str,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
verify_ssl: bool = True,
|
||||
) -> None:
|
||||
super().__init__(registry_url, username, password, token, verify_ssl)
|
||||
self._base_url = self._normalise_url(registry_url)
|
||||
self._bearer_token = None
|
||||
self._bearer_token: str | None = None
|
||||
self._basic_auth_verified = False
|
||||
|
||||
@staticmethod
|
||||
def _normalise_url(url):
|
||||
def _normalise_url(url: str) -> str:
|
||||
url = url.rstrip("/")
|
||||
if not url.startswith(("http://", "https://")):
|
||||
url = f"https://{url}"
|
||||
return url
|
||||
|
||||
def list_repositories(self):
|
||||
def list_repositories(self) -> list[str]:
|
||||
self._ensure_auth()
|
||||
repositories = []
|
||||
repositories: list[str] = []
|
||||
url = f"{self._base_url}/v2/_catalog"
|
||||
params = {"n": 200}
|
||||
params: dict = {"n": 200}
|
||||
while url:
|
||||
resp = self._authed_request("GET", url, params=params)
|
||||
if resp.status_code == 404:
|
||||
@@ -50,11 +57,11 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
params = {}
|
||||
return repositories
|
||||
|
||||
def list_tags(self, repository):
|
||||
def list_tags(self, repository: str) -> list[str]:
|
||||
self._ensure_auth(repository=repository)
|
||||
tags = []
|
||||
tags: list[str] = []
|
||||
url = f"{self._base_url}/v2/{repository}/tags/list"
|
||||
params = {"n": 200}
|
||||
params: dict = {"n": 200}
|
||||
while url:
|
||||
resp = self._authed_request("GET", url, params=params)
|
||||
self._check_response(resp, f"tag listing for {repository}")
|
||||
@@ -64,7 +71,7 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
params = {}
|
||||
return tags
|
||||
|
||||
def _ensure_auth(self, repository=None):
|
||||
def _ensure_auth(self, repository: str | None = None) -> None:
|
||||
if self._bearer_token:
|
||||
return
|
||||
if self._basic_auth_verified:
|
||||
@@ -101,8 +108,14 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
file=__file__,
|
||||
message=f"Access denied to registry {self.registry_url} (HTTP 403). Check REGISTRY_USERNAME and REGISTRY_PASSWORD.",
|
||||
)
|
||||
raise ImageRegistryNetworkError(
|
||||
file=__file__,
|
||||
message=f"Unexpected HTTP {resp.status_code} from registry {self.registry_url} during auth check.",
|
||||
)
|
||||
|
||||
def _obtain_bearer_token(self, www_authenticate, repository=None):
|
||||
def _obtain_bearer_token(
|
||||
self, www_authenticate: str, repository: str | None = None
|
||||
) -> str:
|
||||
match = re.search(r'realm="([^"]+)"', www_authenticate)
|
||||
if not match:
|
||||
raise ImageRegistryAuthError(
|
||||
@@ -110,7 +123,7 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
message=f"Cannot parse token endpoint from registry {self.registry_url}. Www-Authenticate: {www_authenticate[:200]}",
|
||||
)
|
||||
realm = match.group(1)
|
||||
params = {}
|
||||
params: dict = {}
|
||||
service_match = re.search(r'service="([^"]+)"', www_authenticate)
|
||||
if service_match:
|
||||
params["service"] = service_match.group(1)
|
||||
@@ -131,7 +144,7 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
data = resp.json()
|
||||
return data.get("token") or data.get("access_token", "")
|
||||
|
||||
def _resolve_basic_credentials(self):
|
||||
def _resolve_basic_credentials(self) -> tuple[str | None, str | None]:
|
||||
"""Decode pre-encoded base64 auth tokens (e.g., from aws ecr get-authorization-token).
|
||||
|
||||
Returns (username, password) — decoded if the password is a base64 token
|
||||
@@ -145,7 +158,7 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
pass
|
||||
return self.username, self.password
|
||||
|
||||
def _authed_request(self, method, url, **kwargs):
|
||||
def _authed_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
||||
headers = kwargs.pop("headers", {})
|
||||
if self._bearer_token:
|
||||
headers["Authorization"] = f"Bearer {self._bearer_token}"
|
||||
@@ -155,7 +168,7 @@ class OciRegistryAdapter(RegistryAdapter):
|
||||
kwargs["headers"] = headers
|
||||
return self._request_with_retry(method, url, **kwargs)
|
||||
|
||||
def _check_response(self, resp, context):
|
||||
def _check_response(self, resp: requests.Response, context: str) -> None:
|
||||
if resp.status_code == 200:
|
||||
return
|
||||
if resp.status_code in (401, 403):
|
||||
|
||||
@@ -1,90 +1,210 @@
|
||||
from argparse import Namespace
|
||||
|
||||
import pytest
|
||||
|
||||
from prowler.providers.image.lib.arguments.arguments import validate_arguments
|
||||
|
||||
|
||||
class TestValidateArguments:
|
||||
def test_no_source_fails(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry=None, image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "--image" in msg
|
||||
|
||||
def test_image_only_passes(self):
|
||||
args = Namespace(images=["nginx:latest"], image_list_file=None, registry=None, image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=["nginx:latest"],
|
||||
image_list_file=None,
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
def test_image_list_only_passes(self):
|
||||
args = Namespace(images=[], image_list_file="images.txt", registry=None, image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file="images.txt",
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
def test_registry_only_passes(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry="myregistry.io", image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry="myregistry.io",
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
def test_image_filter_without_registry_fails(self):
|
||||
args = Namespace(images=["nginx:latest"], image_list_file=None, registry=None, image_filter="^prod", tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=["nginx:latest"],
|
||||
image_list_file=None,
|
||||
registry=None,
|
||||
image_filter="^prod",
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "--image-filter requires --registry" in msg
|
||||
|
||||
def test_tag_filter_without_registry_fails(self):
|
||||
args = Namespace(images=["nginx:latest"], image_list_file=None, registry=None, image_filter=None, tag_filter="^v", max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=["nginx:latest"],
|
||||
image_list_file=None,
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter="^v",
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "--tag-filter requires --registry" in msg
|
||||
|
||||
def test_max_images_without_registry_fails(self):
|
||||
args = Namespace(images=["nginx:latest"], image_list_file=None, registry=None, image_filter=None, tag_filter=None, max_images=50, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=["nginx:latest"],
|
||||
image_list_file=None,
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=50,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "--max-images requires --registry" in msg
|
||||
|
||||
def test_registry_insecure_without_registry_fails(self):
|
||||
args = Namespace(images=[], image_list_file="i.txt", registry=None, image_filter=None, tag_filter=None, max_images=0, registry_insecure=True)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file="i.txt",
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=True,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "--registry-insecure requires --registry" in msg
|
||||
|
||||
def test_docker_hub_no_namespace_fails(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry="docker.io", image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry="docker.io",
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "namespace" in msg.lower()
|
||||
|
||||
def test_docker_hub_with_namespace_passes(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry="docker.io/myorg", image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry="docker.io/myorg",
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
def test_docker_hub_https_no_namespace_fails(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry="https://docker.io", image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry="https://docker.io",
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "namespace" in msg.lower()
|
||||
|
||||
def test_registry_with_filters_passes(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry="myregistry.io", image_filter="^prod", tag_filter="^v", max_images=100, registry_insecure=True)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry="myregistry.io",
|
||||
image_filter="^prod",
|
||||
tag_filter="^v",
|
||||
max_images=100,
|
||||
registry_insecure=True,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
def test_registry_list_without_registry_fails(self):
|
||||
args = Namespace(images=["nginx:latest"], image_list_file=None, registry=None, image_filter=None, tag_filter=None, max_images=0, registry_insecure=False, registry_list_images=True)
|
||||
args = Namespace(
|
||||
images=["nginx:latest"],
|
||||
image_list_file=None,
|
||||
registry=None,
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
registry_list_images=True,
|
||||
)
|
||||
ok, msg = validate_arguments(args)
|
||||
assert not ok
|
||||
assert "--registry-list requires --registry" in msg
|
||||
|
||||
def test_registry_list_with_registry_passes(self):
|
||||
args = Namespace(images=[], image_list_file=None, registry="myregistry.io", image_filter=None, tag_filter=None, max_images=0, registry_insecure=False, registry_list_images=True)
|
||||
args = Namespace(
|
||||
images=[],
|
||||
image_list_file=None,
|
||||
registry="myregistry.io",
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
registry_list_images=True,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
def test_combined_registry_and_image_passes(self):
|
||||
args = Namespace(images=["nginx:latest"], image_list_file=None, registry="myregistry.io", image_filter=None, tag_filter=None, max_images=0, registry_insecure=False)
|
||||
args = Namespace(
|
||||
images=["nginx:latest"],
|
||||
image_list_file=None,
|
||||
registry="myregistry.io",
|
||||
image_filter=None,
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
)
|
||||
ok, _ = validate_arguments(args)
|
||||
assert ok
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter
|
||||
from prowler.providers.image.lib.registry.factory import (
|
||||
create_registry_adapter,
|
||||
@@ -27,10 +23,15 @@ class TestDetectRegistryType:
|
||||
assert detect_registry_type("https://docker.io/myorg") == "dockerhub"
|
||||
|
||||
def test_ecr(self):
|
||||
assert detect_registry_type("123456789.dkr.ecr.us-east-1.amazonaws.com") == "ecr"
|
||||
assert (
|
||||
detect_registry_type("123456789.dkr.ecr.us-east-1.amazonaws.com") == "ecr"
|
||||
)
|
||||
|
||||
def test_ecr_with_https(self):
|
||||
assert detect_registry_type("https://123456789.dkr.ecr.us-east-1.amazonaws.com") == "ecr"
|
||||
assert (
|
||||
detect_registry_type("https://123456789.dkr.ecr.us-east-1.amazonaws.com")
|
||||
== "ecr"
|
||||
)
|
||||
|
||||
def test_harbor(self):
|
||||
assert detect_registry_type("harbor.example.com") == "oci"
|
||||
@@ -51,7 +52,11 @@ class TestCreateRegistryAdapter:
|
||||
|
||||
def test_passes_credentials(self):
|
||||
adapter = create_registry_adapter(
|
||||
"myregistry.io", username="user", password="pass", token="tok", verify_ssl=False
|
||||
"myregistry.io",
|
||||
username="user",
|
||||
password="pass",
|
||||
token="tok",
|
||||
verify_ssl=False,
|
||||
)
|
||||
assert adapter.username == "user"
|
||||
assert adapter.password == "pass"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
Reference in New Issue
Block a user