From 0664b90ad4bbd713e5cca34d3d154909ce815448 Mon Sep 17 00:00:00 2001 From: patchwright Date: Fri, 3 Jul 2026 04:10:46 +0200 Subject: [PATCH 1/2] fix: handle legacy version strings in potentially_compromised_email_domain version.parse raises InvalidVersion on non-PEP-440 strings (e.g. pytz '2004d') since packaging 22.0 removed LegacyVersion. This crashed the entire rule when scanning packages with legacy version history. Add _safe_version_key that catches InvalidVersion and sorts legacy versions as oldest (version 0) instead of crashing. Fixes #389. Assisted-by: Claude (Anthropic). Co-Authored-By: claude-flow --- .../pypi/potentially_compromised_email_domain.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py b/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py index 9685f5a4b..290ccefc6 100644 --- a/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py +++ b/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py @@ -8,6 +8,7 @@ from dateutil import parser from packaging import version +from packaging.version import InvalidVersion from guarddog.analyzer.metadata.potentially_compromised_email_domain import ( PotentiallyCompromisedEmailDomainDetector, @@ -16,6 +17,19 @@ from .utils import get_email_addresses +def _safe_version_key(v: str): + """Sort key that tolerates legacy version strings (e.g. ``2004d``). + + ``packaging`` 22.0 removed ``LegacyVersion``, so ``version.parse`` raises + ``InvalidVersion`` on non-PEP-440 strings. Return a minimum sentinel so + legacy versions sort as oldest rather than crashing the whole rule (#389). + """ + try: + return version.parse(v) + except InvalidVersion: + return version.parse("0") + + class PypiPotentiallyCompromisedEmailDomainDetector( PotentiallyCompromisedEmailDomainDetector ): @@ -37,7 +51,7 @@ def get_project_latest_release_date(self, package_info) -> Optional[datetime]: """ releases = package_info["releases"] sorted_versions = sorted( - releases.keys(), key=lambda r: version.parse(r), reverse=True + releases.keys(), key=_safe_version_key, reverse=True ) earlier_versions = ( sorted_versions[:-1] if len(sorted_versions) > 1 else sorted_versions From 0eeca5f2149f48186320e054ed5370589586349a Mon Sep 17 00:00:00 2001 From: Christophe Tafani-Dereeper Date: Wed, 8 Jul 2026 16:22:12 +0200 Subject: [PATCH 2/2] Add regression test for legacy version strings --- .../potentially_compromised_email_domain.py | 4 +-- ...st_potentially_compromised_email_domain.py | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py b/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py index 290ccefc6..04b7feef0 100644 --- a/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py +++ b/guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py @@ -50,9 +50,7 @@ def get_project_latest_release_date(self, package_info) -> Optional[datetime]: datetime: creation date of the most recent in releases """ releases = package_info["releases"] - sorted_versions = sorted( - releases.keys(), key=_safe_version_key, reverse=True - ) + sorted_versions = sorted(releases.keys(), key=_safe_version_key, reverse=True) earlier_versions = ( sorted_versions[:-1] if len(sorted_versions) > 1 else sorted_versions ) diff --git a/tests/analyzer/metadata/test_potentially_compromised_email_domain.py b/tests/analyzer/metadata/test_potentially_compromised_email_domain.py index ae0dfc02e..e874917a0 100644 --- a/tests/analyzer/metadata/test_potentially_compromised_email_domain.py +++ b/tests/analyzer/metadata/test_potentially_compromised_email_domain.py @@ -20,7 +20,6 @@ from tests.analyzer.metadata.utils import MockWhoIs - with open( os.path.join(pathlib.Path(__file__).parent.resolve(), "resources", "npm_data.json"), "r", @@ -110,3 +109,37 @@ def test_single_package_version(self): pass # we expect no exception to be thrown except Exception as e: pytest.fail(f"Unexpected exception thrown: {e}") + + def test_legacy_version_string(self): + """ + Regression test for https://github.com/DataDog/guarddog/issues/389 + + packaging >= 22.0 raises InvalidVersion on non-PEP-440 strings + (e.g. pytz's "2004d"), which used to crash the whole rule. + """ + + def mock_whois(domain): + return MockWhoIs(datetime(1990, 1, 31)) + + MonkeyPatch().setattr("whois.whois", mock_whois) + + current_info = deepcopy(PYPI_PACKAGE_INFO) + current_info["releases"] = { + "2004d": [ + { + "upload_time": "2004-04-01T00:41:25", + "upload_time_iso_8601": "2004-04-01T00:41:25.953817Z", + } + ], + "2023.3": [ + { + "upload_time": "2023-03-06T00:41:25", + "upload_time_iso_8601": "2023-03-06T00:41:25.953817Z", + } + ], + } + try: + pypi_detector.detect(current_info) + pass # we expect no exception to be thrown + except Exception as e: + pytest.fail(f"Unexpected exception thrown: {e}")