"""Tests for ledgrab.utils.net_classify.""" import pytest from ledgrab.utils.net_classify import ( HostCategory, classify_ip, is_blocked_for_ssrf, is_local_for_http_default, is_loopback, ) @pytest.mark.parametrize( "host,expected", [ ("127.0.0.1", HostCategory.LOOPBACK), ("::1", HostCategory.LOOPBACK), ("10.0.0.5", HostCategory.PRIVATE), ("192.168.1.1", HostCategory.PRIVATE), ("172.16.0.5", HostCategory.PRIVATE), ("fd00::1", HostCategory.PRIVATE), # ULA ("169.254.1.1", HostCategory.LINK_LOCAL), ("fe80::1", HostCategory.LINK_LOCAL), ("0.0.0.0", HostCategory.UNSPECIFIED), ("::", HostCategory.UNSPECIFIED), ("224.0.0.1", HostCategory.MULTICAST), ("ff00::1", HostCategory.MULTICAST), # ``240.0.0.0/4`` (class E) is labelled both ``is_private`` and # ``is_reserved`` by Python; we keep PRIVATE first which is the # stricter SSRF policy. ("240.0.0.1", HostCategory.PRIVATE), ("8.8.8.8", HostCategory.PUBLIC), ("1.1.1.1", HostCategory.PUBLIC), ("2606:4700:4700::1111", HostCategory.PUBLIC), ("not-an-ip", HostCategory.UNPARSEABLE), ("", HostCategory.UNPARSEABLE), ("example.com", HostCategory.UNPARSEABLE), ], ) def test_classify_ip(host: str, expected: HostCategory) -> None: assert classify_ip(host) is expected # --------------------------------------------------------------------------- # SSRF block list — must include EVERY non-public category, including # unparseable inputs. Regression guard: if anyone narrows this set we lose # SSRF protection. # --------------------------------------------------------------------------- @pytest.mark.parametrize( "host", [ "127.0.0.1", "::1", "10.0.0.5", "192.168.1.1", "172.16.0.5", "fd00::1", "169.254.1.1", "fe80::1", "0.0.0.0", "::", "224.0.0.1", "ff00::1", "240.0.0.1", "not-an-ip", # unparseable → blocked "", # unparseable → blocked ], ) def test_is_blocked_for_ssrf_blocks_non_public(host: str) -> None: assert is_blocked_for_ssrf(host) is True @pytest.mark.parametrize( "host", ["8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"], ) def test_is_blocked_for_ssrf_allows_public(host: str) -> None: assert is_blocked_for_ssrf(host) is False # --------------------------------------------------------------------------- # LAN-default policy — narrower than SSRF: we infer ``http://`` for loopback # / private / link-local / unspecified, NOT for multicast / reserved / # unparseable (those should fall through to ``https://`` or to caller-side # heuristics like mDNS suffix matching). # --------------------------------------------------------------------------- @pytest.mark.parametrize( "host", ["127.0.0.1", "::1", "10.0.0.5", "192.168.1.1", "fe80::1", "0.0.0.0"], ) def test_is_local_for_http_default_true(host: str) -> None: assert is_local_for_http_default(host) is True @pytest.mark.parametrize( "host", ["8.8.8.8", "224.0.0.1", "not-an-ip", ""], ) def test_is_local_for_http_default_false(host: str) -> None: assert is_local_for_http_default(host) is False # --------------------------------------------------------------------------- # Loopback predicate — accepts both literals and the auth module's textual # placeholders (localhost, testclient). # --------------------------------------------------------------------------- @pytest.mark.parametrize( "host", [ "127.0.0.1", "127.0.0.5", "::1", "localhost", "LOCALHOST", "testclient", "[::1]", "fe80::1%eth0", # ← link-local with zone — must NOT match ], ) def test_is_loopback_recognises_loopback(host: str) -> None: expected = host != "fe80::1%eth0" assert is_loopback(host) is expected @pytest.mark.parametrize( "host", ["8.8.8.8", "10.0.0.5", "example.com", "", None], ) def test_is_loopback_rejects_other(host) -> None: assert is_loopback(host) is False