mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
feat(image): add --registry-list flag for image discovery
Add --registry-list flag that enumerates registry repositories and tags, then prints a structured listing and exits without scanning. This helps users discover available images before building --image-filter or --tag-filter regexes.
This commit is contained in:
@@ -295,6 +295,9 @@ class Provider(ABC):
|
||||
registry_insecure=getattr(
|
||||
arguments, "registry_insecure", False
|
||||
),
|
||||
registry_list_images=getattr(
|
||||
arguments, "registry_list_images", False
|
||||
),
|
||||
)
|
||||
elif "mongodbatlas" in provider_class_name.lower():
|
||||
provider_class(
|
||||
|
||||
@@ -80,6 +80,7 @@ class ImageProvider(Provider):
|
||||
tag_filter: str | None = None,
|
||||
max_images: int = 0,
|
||||
registry_insecure: bool = False,
|
||||
registry_list_images: bool = False,
|
||||
):
|
||||
logger.info("Instantiating Image Provider...")
|
||||
|
||||
@@ -122,6 +123,7 @@ class ImageProvider(Provider):
|
||||
self.tag_filter = tag_filter
|
||||
self.max_images = max_images
|
||||
self.registry_insecure = registry_insecure
|
||||
self.registry_list_images = registry_list_images
|
||||
|
||||
# Compile regex filters
|
||||
self._image_filter_re = None
|
||||
@@ -819,6 +821,7 @@ class ImageProvider(Provider):
|
||||
is_dockerhub = isinstance(adapter, DockerHubAdapter)
|
||||
|
||||
discovered_images = []
|
||||
repos_tags: dict[str, list[str]] = {}
|
||||
for repo in repositories:
|
||||
tags = adapter.list_tags(repo)
|
||||
|
||||
@@ -826,6 +829,9 @@ class ImageProvider(Provider):
|
||||
if self._tag_filter_re:
|
||||
tags = [t for t in tags if self._tag_filter_re.search(t)]
|
||||
|
||||
if tags:
|
||||
repos_tags[repo] = tags
|
||||
|
||||
for tag in tags:
|
||||
if is_dockerhub:
|
||||
# Docker Hub images don't need a host prefix
|
||||
@@ -840,6 +846,11 @@ class ImageProvider(Provider):
|
||||
image_ref = f"{registry_host}/{repo}:{tag}"
|
||||
discovered_images.append(image_ref)
|
||||
|
||||
# Registry list mode: print listing and exit
|
||||
if self.registry_list_images:
|
||||
self._print_registry_listing(repos_tags, len(discovered_images))
|
||||
raise SystemExit(0)
|
||||
|
||||
# Check max-images limit
|
||||
if self.max_images and len(discovered_images) > self.max_images:
|
||||
raise ImageMaxImagesExceededError(
|
||||
@@ -859,6 +870,24 @@ class ImageProvider(Provider):
|
||||
f"({len(repositories)} repositories). Total images to scan: {len(self.images)}"
|
||||
)
|
||||
|
||||
def _print_registry_listing(
|
||||
self, repos_tags: dict[str, list[str]], total_images: int
|
||||
) -> None:
|
||||
"""Print a structured listing of registry repositories and tags."""
|
||||
num_repos = len(repos_tags)
|
||||
print(
|
||||
f"\n{Style.BRIGHT}Registry:{Style.RESET_ALL} "
|
||||
f"{Fore.CYAN}{self.registry}{Style.RESET_ALL} "
|
||||
f"({num_repos} repositories, {total_images} images)\n"
|
||||
)
|
||||
for repo, tags in repos_tags.items():
|
||||
print(
|
||||
f" {Fore.YELLOW}{repo}{Style.RESET_ALL} "
|
||||
f"({len(tags)} tags)"
|
||||
)
|
||||
print(f" {', '.join(tags)}")
|
||||
print()
|
||||
|
||||
def print_credentials(self) -> None:
|
||||
"""Print scan configuration."""
|
||||
report_title = f"{Style.BRIGHT}Scanning container images:{Style.RESET_ALL}"
|
||||
|
||||
@@ -122,6 +122,13 @@ def init_parser(self):
|
||||
default=False,
|
||||
help="Skip TLS verification for registry connections (for self-signed certificates).",
|
||||
)
|
||||
registry_group.add_argument(
|
||||
"--registry-list",
|
||||
dest="registry_list_images",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="List all repositories and tags from the registry, then exit without scanning. Useful for discovering available images before building --image-filter or --tag-filter.",
|
||||
)
|
||||
|
||||
|
||||
def validate_arguments(arguments):
|
||||
@@ -133,6 +140,10 @@ def validate_arguments(arguments):
|
||||
tag_filter = getattr(arguments, "tag_filter", None)
|
||||
max_images = getattr(arguments, "max_images", 0)
|
||||
registry_insecure = getattr(arguments, "registry_insecure", False)
|
||||
registry_list_images = getattr(arguments, "registry_list_images", False)
|
||||
|
||||
if registry_list_images and not registry:
|
||||
return (False, "--registry-list requires --registry.")
|
||||
|
||||
if not images and not image_list_file and not registry:
|
||||
return (
|
||||
|
||||
@@ -24,6 +24,7 @@ def _build_provider(**overrides):
|
||||
tag_filter=None,
|
||||
max_images=0,
|
||||
registry_insecure=False,
|
||||
registry_list_images=False,
|
||||
config_content={"image": {}},
|
||||
)
|
||||
defaults.update(overrides)
|
||||
@@ -147,3 +148,71 @@ class TestEmptyRegistry:
|
||||
|
||||
provider = _build_provider(images=["nginx:latest"])
|
||||
assert provider.images == ["nginx:latest"]
|
||||
|
||||
|
||||
class TestRegistryList:
|
||||
@patch("prowler.providers.image.image_provider.create_registry_adapter")
|
||||
def test_registry_list_prints_and_exits(self, mock_factory, capsys):
|
||||
adapter = MagicMock()
|
||||
adapter.list_repositories.return_value = ["app/frontend", "app/backend"]
|
||||
adapter.list_tags.side_effect = [["latest", "v1.0"], ["latest"]]
|
||||
mock_factory.return_value = adapter
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_build_provider(registry_list_images=True)
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "app/frontend" in captured.out
|
||||
assert "app/backend" in captured.out
|
||||
assert "latest" in captured.out
|
||||
assert "v1.0" in captured.out
|
||||
assert "2 repositories" in captured.out
|
||||
assert "3 images" in captured.out
|
||||
|
||||
@patch("prowler.providers.image.image_provider.create_registry_adapter")
|
||||
def test_registry_list_respects_image_filter(self, mock_factory, capsys):
|
||||
adapter = MagicMock()
|
||||
adapter.list_repositories.return_value = ["prod/app", "dev/app"]
|
||||
adapter.list_tags.return_value = ["latest"]
|
||||
mock_factory.return_value = adapter
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_build_provider(registry_list_images=True, image_filter="^prod/")
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "prod/app" in captured.out
|
||||
assert "dev/app" not in captured.out
|
||||
assert "1 repositories" in captured.out
|
||||
|
||||
@patch("prowler.providers.image.image_provider.create_registry_adapter")
|
||||
def test_registry_list_respects_tag_filter(self, mock_factory, capsys):
|
||||
adapter = MagicMock()
|
||||
adapter.list_repositories.return_value = ["myapp"]
|
||||
adapter.list_tags.return_value = ["latest", "v1.0", "dev-abc"]
|
||||
mock_factory.return_value = adapter
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_build_provider(registry_list_images=True, tag_filter=r"^v\d+\.\d+$")
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "v1.0" in captured.out
|
||||
assert "dev-abc" not in captured.out
|
||||
assert "1 images" in captured.out
|
||||
|
||||
@patch("prowler.providers.image.image_provider.create_registry_adapter")
|
||||
def test_registry_list_skips_max_images(self, mock_factory, capsys):
|
||||
adapter = MagicMock()
|
||||
adapter.list_repositories.return_value = ["app1", "app2", "app3"]
|
||||
adapter.list_tags.return_value = ["latest", "v1.0"]
|
||||
mock_factory.return_value = adapter
|
||||
|
||||
# max_images=1 would normally raise, but --registry-list skips it
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_build_provider(registry_list_images=True, max_images=1)
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "6 images" in captured.out
|
||||
|
||||
Reference in New Issue
Block a user