@@ -53,7 +53,7 @@ def test_image_provider(self):
5353 assert provider ._type == "image"
5454 assert provider .type == "image"
5555 assert provider .images == ["alpine:3.18" ]
56- assert provider .scanners == ["vuln" , "secret" ]
56+ assert provider .scanners == ["vuln" , "secret" , "misconfig" ]
5757 assert provider .image_config_scanners == []
5858 assert provider .trivy_severity == []
5959 assert provider .ignore_unfixed is False
@@ -698,8 +698,118 @@ def test_bare_image_name(self):
698698
699699
700700class TestIsRegistryUrl :
701- def test_registry_url_with_namespace (self ):
702- assert ImageProvider ._is_registry_url ("docker.io/andoniaf" ) is True
701+ def test_bare_ecr_hostname (self ):
702+ assert ImageProvider ._is_registry_url (
703+ "714274078102.dkr.ecr.eu-west-1.amazonaws.com"
704+ )
705+
706+ def test_bare_hostname_with_port (self ):
707+ assert ImageProvider ._is_registry_url ("myregistry.com:5000" )
708+
709+ def test_bare_ghcr (self ):
710+ assert ImageProvider ._is_registry_url ("ghcr.io" )
711+
712+ def test_registry_with_namespace_only (self ):
713+ """Registry URL with a single path segment (no tag) is a registry URL."""
714+ assert ImageProvider ._is_registry_url ("ghcr.io/myorg" )
715+
716+ def test_image_reference_not_registry (self ):
717+ """Full image reference with repo and tag is not a registry URL."""
718+ assert not ImageProvider ._is_registry_url ("ghcr.io/myorg/repo:tag" )
719+
720+ def test_simple_image_name (self ):
721+ assert not ImageProvider ._is_registry_url ("alpine:3.18" )
722+
723+ def test_bare_image_no_tag (self ):
724+ assert not ImageProvider ._is_registry_url ("nginx" )
725+
726+ def test_dockerhub_namespace (self ):
727+ assert not ImageProvider ._is_registry_url ("library/alpine" )
728+
729+
730+ class TestTestRegistryConnection :
731+ @patch ("prowler.providers.image.image_provider.create_registry_adapter" )
732+ def test_registry_connection_success (self , mock_factory ):
733+ """Test that a bare hostname triggers registry catalog test."""
734+ mock_adapter = MagicMock ()
735+ mock_adapter .list_repositories .return_value = ["repo1" ]
736+ mock_factory .return_value = mock_adapter
737+
738+ result = ImageProvider .test_connection (
739+ image = "714274078102.dkr.ecr.eu-west-1.amazonaws.com" ,
740+ registry_username = "user" ,
741+ registry_password = "pass" ,
742+ )
743+
744+ assert result .is_connected is True
745+ mock_factory .assert_called_once_with (
746+ registry_url = "714274078102.dkr.ecr.eu-west-1.amazonaws.com" ,
747+ username = "user" ,
748+ password = "pass" ,
749+ token = None ,
750+ )
751+ mock_adapter .list_repositories .assert_called_once ()
752+
753+ @patch ("prowler.providers.image.image_provider.create_registry_adapter" )
754+ def test_registry_connection_auth_failure (self , mock_factory ):
755+ """Test that 401 from registry adapter returns auth failure."""
756+ mock_adapter = MagicMock ()
757+ mock_adapter .list_repositories .side_effect = Exception ("401 unauthorized" )
758+ mock_factory .return_value = mock_adapter
759+
760+ result = ImageProvider .test_connection (
761+ image = "714274078102.dkr.ecr.eu-west-1.amazonaws.com" ,
762+ )
763+
764+ assert result .is_connected is False
765+ assert "Authentication failed" in result .error
766+
767+ @patch ("prowler.providers.image.image_provider.create_registry_adapter" )
768+ def test_registry_connection_generic_error (self , mock_factory ):
769+ """Test that a generic error from registry adapter returns error message."""
770+ mock_adapter = MagicMock ()
771+ mock_adapter .list_repositories .side_effect = Exception ("connection refused" )
772+ mock_factory .return_value = mock_adapter
773+
774+ result = ImageProvider .test_connection (
775+ image = "myregistry.example.com" ,
776+ )
777+
778+ assert result .is_connected is False
779+ assert "Failed to connect to registry" in result .error
780+
781+ @patch ("prowler.providers.image.image_provider.create_registry_adapter" )
782+ def test_image_reference_uses_registry_adapter (self , mock_factory ):
783+ """Test that a full image reference uses registry adapter to verify tag."""
784+ mock_adapter = MagicMock ()
785+ mock_adapter .list_tags .return_value = ["3.18" , "latest" ]
786+ mock_factory .return_value = mock_adapter
787+
788+ result = ImageProvider .test_connection (image = "alpine:3.18" )
789+
790+ assert result .is_connected is True
791+ mock_adapter .list_tags .assert_called_once ()
792+
793+
794+ class TestTrivyAuthIntegration :
795+ @patch ("subprocess.run" )
796+ def test_run_scan_passes_trivy_env_with_credentials (self , mock_subprocess ):
797+ """Test that run_scan() passes TRIVY_USERNAME/PASSWORD via env when credentials are set."""
798+ mock_subprocess .return_value = MagicMock (
799+ returncode = 0 , stdout = get_sample_trivy_json_output (), stderr = ""
800+ )
801+ provider = _make_provider (
802+ images = ["ghcr.io/user/image:tag" ],
803+ registry_username = "myuser" ,
804+ registry_password = "mypass" ,
805+ )
806+
807+ list (provider .run_scan ())
808+
809+ call_kwargs = mock_subprocess .call_args
810+ env = call_kwargs .kwargs .get ("env" ) or call_kwargs [1 ].get ("env" )
811+ assert env ["TRIVY_USERNAME" ] == "myuser"
812+ assert env ["TRIVY_PASSWORD" ] == "mypass"
703813
704814 def test_registry_url_ghcr (self ):
705815 assert ImageProvider ._is_registry_url ("ghcr.io/org" ) is True
@@ -734,6 +844,16 @@ def test_cleanup_idempotent(self):
734844 provider .cleanup ()
735845 provider .cleanup ()
736846
847+ def test_cleanup_removes_trivy_cache_dir (self ):
848+ """Test that cleanup removes the temporary Trivy cache directory."""
849+ provider = _make_provider ()
850+ cache_dir = provider ._trivy_cache_dir
851+ assert os .path .isdir (cache_dir )
852+
853+ provider .cleanup ()
854+
855+ assert not os .path .isdir (cache_dir )
856+
737857
738858class TestImageProviderInputValidation :
739859 def test_invalid_timeout_format_raises_error (self ):
@@ -804,6 +924,22 @@ def test_invalid_image_config_scanner_raises_error(self):
804924 with pytest .raises (ImageInvalidConfigScannerError ):
805925 _make_provider (image_config_scanners = ["misconfig" , "vuln" ])
806926
927+ @patch ("subprocess.run" )
928+ def test_trivy_command_includes_cache_dir (self , mock_subprocess ):
929+ """Test that Trivy command includes --cache-dir for cache isolation."""
930+ provider = _make_provider ()
931+ mock_subprocess .return_value = MagicMock (
932+ returncode = 0 , stdout = get_empty_trivy_output (), stderr = ""
933+ )
934+
935+ for _ in provider ._scan_single_image ("alpine:3.18" ):
936+ pass
937+
938+ call_args = mock_subprocess .call_args [0 ][0 ]
939+ assert "--cache-dir" in call_args
940+ idx = call_args .index ("--cache-dir" )
941+ assert call_args [idx + 1 ] == provider ._trivy_cache_dir
942+
807943 @patch ("subprocess.run" )
808944 def test_trivy_command_includes_image_config_scanners (self , mock_subprocess ):
809945 """Test that Trivy command includes --image-config-scanners when set."""
0 commit comments