diff --git a/nmapui/validation.py b/nmapui/validation.py index 199ecf0f..db9aae70 100644 --- a/nmapui/validation.py +++ b/nmapui/validation.py @@ -7,6 +7,26 @@ r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?" r"(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$" ) +_IPV4_LIKE_RE = re.compile(r"^[0-9./-]+$") + + +def _is_valid_ipv4_range(item: str) -> bool: + """Return whether an item is an Nmap-style IPv4 octet range.""" + octets = item.split(".") + if len(octets) != 4 or not any("-" in octet for octet in octets): + return False + + for octet in octets: + bounds = octet.split("-") + if len(bounds) > 2 or any(not bound.isdigit() for bound in bounds): + return False + values = [int(bound) for bound in bounds] + if any(value > 255 for value in values): + return False + if len(values) == 2 and values[0] > values[1]: + return False + + return True def _is_valid_item(item: str) -> tuple[bool, str | None]: @@ -27,6 +47,14 @@ def _is_valid_item(item: str) -> tuple[bool, str | None]: except ValueError: pass + # Nmap supports ranges within IPv4 octets, such as 192.168.1.1-254. + if _is_valid_ipv4_range(item): + return True, None + + # Do not let malformed dotted IPs fall through and pass as hostnames. + if _IPV4_LIKE_RE.fullmatch(item): + return False, f"Invalid target: {item}" + # Hostname / FQDN (nmap accepts these natively) if _HOSTNAME_RE.match(item): return True, None diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 00000000..e1dd0138 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,35 @@ +import pytest + +from nmapui.validation import validate_target + + +@pytest.mark.parametrize( + "target", + [ + "192.168.1.1", + "192.168.1.0/24", + "192.168.1.1-254", + "10-12.0.0.1-20", + "scanme.nmap.org", + "192.168.1.1, 10.0.0.0/8", + ], +) +def test_validate_target_accepts_supported_target_formats(target): + assert validate_target(target) == (True, None) + + +@pytest.mark.parametrize( + "target", + [ + "192.168.1.999", + "192.168.1", + "192.168.1.1/33", + "192.168.1.254-1", + "192.168.1.1-999", + ], +) +def test_validate_target_rejects_malformed_ip_like_targets(target): + valid, error = validate_target(target) + + assert valid is False + assert error == f"Invalid target: {target}"