mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-19 06:43:28 +02:00
Out-of-band testing is an instance setting — `off`, `self_hosted` or `public` — with the server, the token (the INTERACTSH API key row) and the wait resolved once in `shared/services/oast.py` and read on the stage thread. A mode that cannot be used is off and says why: self-hosted with no server, or public without the disclosure acknowledged. A fresh instance is off. `POST /oast/test` asks a public address for `/poll` over HTTPS and reports that the server answered, never that OAST works. The engine keeps one switch. `FieldNeeds` puts the resource key on the field and `GET /engines/catalog` fills `unavailable_reason`, so the builder renders the instance reason beside a switch that stays settable. Measured on nuclei v3.11.1: with `-no-interactsh` a check that needs a callback still loads and the request is refused — 0 requests, exit 0. So the batch reported COMPLETED and every origin in it read `scanned` for checks that never touched the target. The selection now drops them when nothing is listening, one coverage row reports the count and the reason, and `Tier.OAST` is left out of the planned tiers. Membership is what a check references, not what it is tagged: 615 of the 12,586 official rows reference an interactsh placeholder and 233 of those carry no `oast` tag. `vuln_templates.needs_oast` is parsed at index time; a library resync fills the column. The wait is paid on out-of-band batches alone, which sweep for 30 minutes so it is paid fewer times, and the tier names the server on its own rows. A finding's callback is rendered on the sheet from one label map; the list path is bared to the metadata. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
import ipaddress
|
|
import socket
|
|
from collections.abc import Iterable
|
|
from urllib.parse import SplitResult, urlsplit
|
|
|
|
|
|
def is_registry_routable(value: str) -> bool:
|
|
"""Whether an address or netblock sits in public space an RIR can hold a record for."""
|
|
try:
|
|
net = ipaddress.ip_network(value.strip(), strict=False)
|
|
except ValueError:
|
|
return False
|
|
return net.is_global and not net.is_multicast
|
|
|
|
|
|
def is_public_address(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
"""Whether an address may be reached from a server-side request."""
|
|
return not (
|
|
ip.is_private
|
|
or ip.is_loopback
|
|
or ip.is_link_local
|
|
or ip.is_reserved
|
|
or ip.is_multicast
|
|
or ip.is_unspecified
|
|
)
|
|
|
|
|
|
def validate_public_https_url(raw: str, *, label: str = "URL") -> None:
|
|
parts = urlsplit(raw)
|
|
if parts.scheme != "https" or not parts.hostname:
|
|
msg = f"{label} must be an https:// URL."
|
|
raise ValueError(msg)
|
|
declared = unreadable_port(parts)
|
|
if declared:
|
|
msg = f"{label} carries {declared!r}, which is not a port number."
|
|
raise ValueError(msg)
|
|
try:
|
|
infos = socket.getaddrinfo(parts.hostname, None)
|
|
except OSError as exc:
|
|
msg = f"Cannot resolve {label} host: {exc}"
|
|
raise ValueError(msg) from exc
|
|
for info in infos:
|
|
if not is_public_address(ipaddress.ip_address(info[4][0])):
|
|
msg = f"{label} resolves to a disallowed address."
|
|
raise ValueError(msg)
|
|
|
|
|
|
def cert_covers(
|
|
host: str | None, subject_cn: str | None, sans: Iterable[str] = ()
|
|
) -> bool:
|
|
"""Whether a certificate names the host presenting it."""
|
|
if not host:
|
|
return False
|
|
target = host.strip().lower().rstrip(".")
|
|
for raw in (subject_cn, *sans):
|
|
if not raw:
|
|
continue
|
|
name = str(raw).strip().lower().rstrip(".")
|
|
if name == target:
|
|
return True
|
|
if (
|
|
name.startswith("*.")
|
|
and target.endswith(name[1:])
|
|
and target.count(".") == name.count(".")
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def url_port(parts: SplitResult) -> int | None:
|
|
"""A URL's port, or None when absent or unreadable: reading .port is what raises."""
|
|
try:
|
|
return parts.port
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def unreadable_port(parts: SplitResult) -> str | None:
|
|
"""The authority's port text when it is not a port number, else None."""
|
|
_, declared = split_host_port(parts.netloc.rsplit("@", 1)[-1])
|
|
return declared if declared and url_port(parts) is None else None
|
|
|
|
|
|
def bracketed(host: str) -> str:
|
|
"""An IPv6 literal in brackets. Anything else is returned as it is."""
|
|
return f"[{host}]" if ":" in host and not host.startswith("[") else host
|
|
|
|
|
|
def host_port(host: str, port: int | str) -> str:
|
|
"""An authority a tool can parse."""
|
|
return f"{bracketed(host)}:{port}"
|
|
|
|
|
|
def split_host_port(authority: str) -> tuple[str, str | None]:
|
|
"""The host and port of an authority, with an IPv6 literal unwrapped."""
|
|
value = authority.strip()
|
|
if value.startswith("["):
|
|
literal, _, rest = value.partition("]")
|
|
port = rest[1:] if rest.startswith(":") else ""
|
|
return literal[1:], port or None
|
|
if value.count(":") == 1:
|
|
host, _, port = value.partition(":")
|
|
return host, port or None
|
|
return value, None
|