diff --git a/prowler/providers/iac/lib/git_transport.py b/prowler/providers/iac/lib/git_transport.py index a181d8c051..ef0ca09d9c 100644 --- a/prowler/providers/iac/lib/git_transport.py +++ b/prowler/providers/iac/lib/git_transport.py @@ -13,7 +13,7 @@ from prowler.lib.network.ssrf import validate_outbound_url # shared so the connection test and the clone cannot disagree about what a # tenant is allowed to configure -ALLOWED_SCHEMES = ("http", "https", "ssh", "git") +ALLOWED_SCHEMES = ("http", "https", "ssh", "git+ssh", "git") class _GuardedRedirects: @@ -51,11 +51,20 @@ def effective_url(config: Config, url: str) -> str: def proxy_base_url(url: str) -> str: - """Scheme and host only, for dulwich's ``no_proxy`` and ``http..*`` matching.""" + """URL without credentials, for dulwich's ``no_proxy`` and ``http..*`` matching. + + Only the userinfo is removed. The path stays, because an ``http..*`` + section can be path-specific and dulwich ranks matches by path length. + """ parsed = urlparse(url) + if parsed.username is None and parsed.password is None: + return url host = parsed.hostname or "" - netloc = f"{host}:{parsed.port}" if parsed.port else host - return urlunparse((parsed.scheme, netloc, "", "", "", "")) + # urlparse drops the brackets of an IPv6 literal and they are not optional + netloc = f"[{host}]" if ":" in host else host + if parsed.port: + netloc = f"{netloc}:{parsed.port}" + return urlunparse(parsed._replace(netloc=netloc)) def guarded_pool_manager(config: Config, base_url: str) -> urllib3.PoolManager: diff --git a/tests/providers/iac/lib/git_transport_test.py b/tests/providers/iac/lib/git_transport_test.py index 63a5893457..39d3b7e251 100644 --- a/tests/providers/iac/lib/git_transport_test.py +++ b/tests/providers/iac/lib/git_transport_test.py @@ -9,6 +9,7 @@ from dulwich.config import ConfigDict from prowler.lib.network.ssrf import ( ALLOWED_PRIVATE_NETWORKS_ENV, OutboundURLNotAllowedError, + validate_outbound_url, ) from prowler.providers.iac.lib import git_transport from prowler.providers.iac.lib.git_transport import ls_remote @@ -121,11 +122,38 @@ class TestEffectiveDestination: with pytest.raises(OutboundURLNotAllowedError, match="non-public"): git_transport.ls_remote("https://reg.example.com/org/repo.git") - def test_keeps_credentials_out_of_the_proxy_base_url(self): - assert ( - git_transport.proxy_base_url("https://user:a-token@github.com:8443/o/r.git") - == "https://github.com:8443" - ) + @pytest.mark.parametrize( + "url,expected", + [ + ( + "https://user:a-token@github.com:8443/o/r.git", + "https://github.com:8443/o/r.git", + ), + # the path stays: an http..* section can be path-specific + ("https://user:a-token@github.com/o/r.git", "https://github.com/o/r.git"), + # urlparse drops the brackets of an IPv6 literal + ("https://user:a-token@[::1]:8443/o/r.git", "https://[::1]:8443/o/r.git"), + # nothing to strip, so nothing is rewritten + ( + "https://[2606:4700:4700::1111]/o/r.git", + "https://[2606:4700:4700::1111]/o/r.git", + ), + ], + ) + def test_strips_only_the_credentials_from_the_proxy_base_url(self, url, expected): + assert git_transport.proxy_base_url(url) == expected + + @pytest.mark.parametrize("scheme", ["http", "https", "ssh", "git+ssh", "git"]) + def test_allows_every_scheme_dulwich_can_transport(self, scheme, monkeypatch): + """A scheme dulwich routes must reach the host check, not be refused on scheme.""" + monkeypatch.setattr(git_transport, "git_config", ConfigDict) + + with _resolves_to("140.82.121.4"): + # the scheme must not be what rejects it; a public host is accepted + validate_outbound_url( + f"{scheme}://host.example/o/r.git", + allowed_schemes=git_transport.ALLOWED_SCHEMES, + ) def test_honours_a_no_proxy_bypass(self, monkeypatch, origin): """Built without base_url, dulwich cannot see no_proxy and uses the proxy."""