diff --git a/src/semantic_release/helpers.py b/src/semantic_release/helpers.py index 5f6723ec4..d8487fda5 100644 --- a/src/semantic_release/helpers.py +++ b/src/semantic_release/helpers.py @@ -10,7 +10,7 @@ from pathlib import Path, PurePosixPath from re import IGNORECASE, compile as regexp from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Sequence, TypeVar -from urllib.parse import urlsplit +from urllib.parse import urlsplit, urlunsplit if TYPE_CHECKING: # pragma: no cover from re import Pattern @@ -215,6 +215,16 @@ class ParsedGitUrl(NamedTuple): repo_name: str +def _hide_credentials_in_url(url: str) -> str: + url_parts = urlsplit(url) + + if not url_parts.scheme or "@" not in url_parts.netloc: + return url + + _, _, host = url_parts.netloc.rpartition("@") + return urlunsplit(url_parts._replace(netloc=f"@{host}")) + + @lru_cache(maxsize=512) def parse_git_url(url: str) -> ParsedGitUrl: """ @@ -242,7 +252,7 @@ def parse_git_url(url: str) -> ParsedGitUrl: Raises ValueError if the url can't be parsed. """ - log.debug("Parsing git url %r", url) + log.debug("Parsing git url %r", _hide_credentials_in_url(url)) # Normalizers are a list of tuples of (pattern, replacement) normalizers = [ diff --git a/tests/unit/semantic_release/test_helpers.py b/tests/unit/semantic_release/test_helpers.py index 4877d3892..5909d49fd 100644 --- a/tests/unit/semantic_release/test_helpers.py +++ b/tests/unit/semantic_release/test_helpers.py @@ -1,4 +1,5 @@ from typing import Iterable +from unittest.mock import patch import pytest @@ -120,6 +121,34 @@ def test_parse_valid_git_urls(url: str, expected: ParsedGitUrl): assert expected == parse_git_url(url) +def test_parse_git_url_does_not_log_credentials(): + """Test that credentials in git urls are masked before logging.""" + username = "x-oauth-basic" + secret = "ghp_secret_token" + url = f"https://{username}:{secret}@github.example.com/owner/project.git" + + expected_parsed_url = ParsedGitUrl( + "https", + f"{username}:{secret}@github.example.com", + "owner", + "project", + ) + + parse_git_url.cache_clear() + with patch("semantic_release.helpers.log") as mock_log: + actual_parsed_url = parse_git_url(url) + + assert expected_parsed_url == actual_parsed_url + assert mock_log.debug.called + + for debug_call in mock_log.debug.call_args_list: + args = debug_call[0] + formatted_msg = args[0] % args[1:] if len(args) > 1 else str(args[0]) + assert secret not in formatted_msg + assert username not in formatted_msg + assert "@github.example.com" in formatted_msg + + @pytest.mark.parametrize( "url", [