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>
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
Three places built a probe target as f"{ip}:{port}". ip_addresses holds
IPv6 — seed_resolution writes both a and aaaa records — so http_probe,
origin_probe and the vulnerability surface handed httpx and nuclei
2001:db8::1:443, which is neither a host nor a host and port. The httpx
parser then made it worse: rsplit(":", 1) turned a bare v6 address into
2001:db8: before it was stored as the asset's host.
The endpoint layer already got this right in two places with the same
inline expression. That expression is now bracketed() in shared/utils/net
alongside host_port() and split_host_port(), and all five sites use it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5roHYDYzSPRNaTkn8hSkJ
Three tools took the scan's network config and dropped part of it.
The banner prober spoke SOCKS5 to whatever proxy it was given. A scan
context holding an http:// proxy made every handshake fail, and the stage
reported "no banner returned by N ports" rather than a problem. It now
speaks SOCKS5 or HTTP CONNECT by scheme (tools/banner/proxy.py, tested
against a stub proxy), and refuses a scheme it cannot carry before any
port is touched instead of dialling direct.
naabu was handed the resolved URL whole. Its -proxy is socks5 only and
takes no inline credentials, so http:// or any password quietly did not
apply. proxy_args() renders -proxy host:port with -proxy-auth beside it,
and names the reason when the proxy cannot be expressed, which port_scan
and host_discovery report as partial. -passive is an HTTPS call to
Shodan, so it goes through proxy_env instead, which passive_ports was not
passing at all.
wafw00f got the proxy but never the headers, so an authenticated surface
was fingerprinted unauthenticated. It takes a file rather than a value,
so the run writes one 0600 temp file and removes it after.
proxy_env moves to shared/services/proxy_resolve.py now that naabu wants
it too, and host_port() lands in shared/utils/net.py for the bracketed
IPv6 authority CONNECT needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5roHYDYzSPRNaTkn8hSkJ
A row states that a favicon, page title, body hash, certificate, CNAME
target or address is also held by a host under a different target, and
the popover names the peers and pivots to the query that returns them.
Read against the estate, so a row on a single-scan page still reaches
the other targets.
The refusal is the feature. Measured across four unrelated estates, all
five cross-target values were a provider's and none is drawn. A carrier
on a SHARED_EDGE_HOSTS CNAME vouches for no identity. generic_page()
refuses a title that restates the status of the rows carrying it, the
soft-404 phrases and the curated edge interstitials, and it gates the
three page-derived kinds. cert_covers() requires a certificate to name
the host presenting it, so *.okta.com on a tenant is not a link. JARM
spanned all four estates and is not a cross link.
Each value is judged once before the row loop and _crossing() refuses to
fetch carriers for a value one target holds: a wildcard CNAME with 8,679
carriers cost 4.6s a page, now ~80ms.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxAonr95Uj7Bdu4CTMCwx9
A registry only holds the registered name, but the lookup asked for the
target verbatim, so every subdomain target 400'd at the registry:
scanme.nmap.org went to the .org RDAP server, which has no such object.
Any target below the apex — api.example.com, www.bbc.co.uk — therefore
carried a permanent "WHOIS failed" badge. Lookups now ask for the
registrable domain, so bbc.co.uk is reached through the public-suffix
table rather than the last two labels.
Private and reserved address space has no registry record at all. That
was reported as a failure carrying the RDAP library's own wording
("IPv4Address 192.168.0.0/24 is private"), which put an error in the
activity feed and a red badge on the target for a fact about the
address. It is now recognised before any query is made and recorded as
not applicable, with a sentence that says why. The target page already
worded this correctly for a single private address; it now recognises a
private range too.
One lookup key is shared by the cache, the stored record and the API
read-back, so a domain and its subdomains resolve to one registry record
instead of re-querying on every miss.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6LcH8uUddjJbKrahf34ys
- Added `clean_name` function to `validation.py` for name validation with max length.
- Simplified docstrings in `dnsx` client, parser, and service files for clarity.
- Enhanced error handling in `ViewDNSService` to prevent disabling keys on rate limits.
- Improved WHOIS service to handle concurrent inserts gracefully with integrity checks.
- Updated `Settings` class in worker config to inherit from `BaseAppSettings`.
- Added new dependencies in `pyproject.toml` for `apprise` and `cryptography`.