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:
Andoni A.
2026-02-17 07:44:41 +01:00
parent 326def4a17
commit 876702fd22
4 changed files with 112 additions and 0 deletions
+3
View File
@@ -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(
+29
View File
@@ -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