Files
rengine/shared/services/scan_surface/normalize.py
T
Yogesh OjhaandClaude Sonnet 5 7a960f1de1 fix(net): a malformed URL port is rejected, not raised, everywhere .port is read
urllib.parse.SplitResult.port raises ValueError when the authority
carries a non-numeric port; nine call sites read it unguarded, so one
malformed URL (a stray quote, a client typo) turned into an unhandled
exception in the middle of a scan, a report, an interest rule or a
proxy dial. shared/utils/net.py:url_port makes the read itself the
failure boundary; unreadable_port names the bogus text so the naabu
and banner proxy paths can report what they refused rather than
retrying against a host string that dropped its port silently.

shared/definitions/ports.py:SCHEME_PORTS replaces four private copies
of the same http/https default-port map (endpoints.py, endpoint_judge,
scan_surface/normalize, nuclei's parser, dalfox's parser).

Also renames live_hosts to answered_hosts in the report source and the
interest context, and lifts the asset query's inline is:web predicate
into predicates.answered() — the existing distinction between is:live
(2xx) and is:web (answered at all) was already the rule; live_hosts
was the one name still claiming the stronger one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
2026-09-16 08:04:06 +05:30

59 lines
1.8 KiB
Python

"""One spelling per target: nuclei deduplicates its input byte for byte."""
from __future__ import annotations
from dataclasses import dataclass
from urllib.parse import urlsplit
from shared.definitions.ports import SCHEME_PORTS as DEFAULT_PORTS
from shared.utils.net import bracketed, host_port, url_port
@dataclass(frozen=True)
class Root:
scheme: str
host: str
port: int
@property
def value(self) -> str:
return root_value(self.scheme, self.host, self.port)
def root_value(scheme: str, host: str, port: int | None) -> str:
"""`scheme://host[:port]`, lowercase, default port omitted, no path."""
scheme = (scheme or "https").lower()
host = (host or "").lower().strip("[]")
literal = bracketed(host)
if port and port != DEFAULT_PORTS.get(scheme):
return f"{scheme}://{literal}:{port}"
return f"{scheme}://{literal}"
def parse_root(
url: str, *, scheme: str | None = None, port: int | None = None
) -> Root | None:
"""The origin a URL names, or None when it names none."""
text = (url or "").strip()
if not text:
return None
if "://" not in text:
text = f"{scheme or 'https'}://{text}"
parsed = urlsplit(text)
host = (parsed.hostname or "").lower()
if not host:
return None
scheme_value = (parsed.scheme or scheme or "https").lower()
port_value = url_port(parsed) or port or DEFAULT_PORTS.get(scheme_value)
if not port_value:
return None
return Root(scheme=scheme_value, host=host, port=int(port_value))
def service_value(host: str, port: int) -> str:
"""`host:port`, the form ssl and network checks take."""
return host_port((host or "").lower(), int(port))
__all__ = ["DEFAULT_PORTS", "Root", "parse_root", "root_value", "service_value"]