Passive domain/infra OSINT over five keyless public APIs — subdomains, RDAP/WHOIS, DNS-over-HTTPS, IP geo/ISP, and ASN/prefix ownership.
---
name: domain-recon
description: Passive domain/infra OSINT over five keyless public APIs — subdomains, RDAP/WHOIS, DNS-over-HTTPS, IP geo/ISP, and ASN/prefix ownership.
metadata: {"openclaw": {"requires": {"bins": ["python3"]}, "emoji": "🛰️"}, "homepage": "https://github.com/maggiedev-bot/domain-recon", "version": "0.6.0"}
---
# domain-recon
Passive reconnaissance for a domain, IP, or ASN using **five keyless public
APIs** — no API keys, no accounts, no secrets. Everything is *passive*: the skill
only queries public third-party databases; it never connects to or probes the
target host directly.
Use this when the user wants to investigate a domain or IP's public footprint:
enumerate subdomains, look up WHOIS/RDAP registration, resolve DNS records, geo-
locate an IP, or identify which network/ASN owns an address.
All work runs through one Python helper (standard library only — no `pip install`):
```bash
python3 {baseDir}/scripts/recon.py <subcommand> <target> [options]
```
Output is JSON by default (easy to parse and chain); add `--human` for a compact
readable summary.
## Subcommands
| Subcommand | Source | What it does |
|------------|--------|--------------|
| `certs <domain>` | crt.sh + certSpotter (Certificate Transparency) | Enumerate subdomains + certificate history; falls back to certSpotter when crt.sh is down |
| `rdap <resource>` | IANA bootstrap + supplement → authoritative server (rdap.org fallback) | Modern WHOIS for a domain, IP, or ASN (auto-detected); resolves ccTLDs like `.ai` (bootstrap) and `.io`/`.sh`/`.ac`/`.us` (supplement) that rdap.org 404s |
| `dns <name>` | Google / Cloudflare DoH | Resolve DNS records (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA) |
| `ip <ip>` | ip-api.com | IP → geo, ISP, ASN, and proxy/hosting flags |
| `asn <resource>` | RIPEstat Data API | ASN → holder + announced prefixes; IP → owning ASN + prefix |
| `wayback <url>` | archive.org | Wayback snapshot availability + CDX capture history |
| `profile <domain>` | all of the above | One-shot orchestration: certs → dns → ip → asn → wayback into a single report |
### Common options
- `--human` — readable text instead of JSON.
- `--timeout <sec>` — per-request timeout (default 20).
- `--retries <n>` — retry attempts on 429/5xx/network errors (default 3).
- `certs --no-wildcards` — drop `*.` wildcard subdomains.
- `dns --type A,MX,TXT` — comma-separated record types in one call.
- `dns --provider cloudflare` — use Cloudflare DoH instead of Google (default).
- `rdap --kind domain|ip|asn` — force the resource kind instead of auto-detecting.
- `asn --no-prefixes` — skip the announced-prefixes list for a faster ASN lookup.
- `certs --limit <n>` / `certs --max-certs <n>` — cap the returned subdomains / cert-history rows (full counts still reported).
- `certs --all-ct` — query **both** CT sources (crt.sh + certSpotter) and merge, instead of stopping at the first that answers (wider coverage).
- `rdap --no-bootstrap` — skip the IANA bootstrap and query rdap.org directly (domains only).
- `wayback --no-cdx` — availability only; `wayback --cdx-limit <n>` — number of recent captures to list.
- `profile --cert-limit <n>` / `--resolve-limit <n>` / `--ip-limit <n>` — bound how much of each stage the orchestrator pulls; `--no-wayback` to skip the archive step.
## Example invocations
```bash
# Subdomains + cert history for a domain
python3 {baseDir}/scripts/recon.py certs example.com --human
# WHOIS/RDAP (auto-detects domain vs IP vs ASN)
python3 {baseDir}/scripts/recon.py rdap example.com
python3 {baseDir}/scripts/recon.py rdap 8.8.8.8
python3 {baseDir}/scripts/recon.py rdap AS15169
# DNS records — several types at once, via Cloudflare
python3 {baseDir}/scripts/recon.py dns example.com --type A,AAAA,MX,TXT --provider cloudflare
# IP geolocation / ISP / hosting flags
python3 {baseDir}/scripts/recon.py ip 8.8.8.8 --human
# Who owns this network? (ASN or IP)
python3 {baseDir}/scripts/recon.py asn AS15169
python3 {baseDir}/scripts/recon.py asn 8.8.8.8
# Is it archived? (availability + recent capture history)
python3 {baseDir}/scripts/recon.py wayback example.com
# One-shot: profile a domain across every source in a single report
python3 {baseDir}/scripts/recon.py profile example.com --human
```
A typical "profile this domain" flow: run `certs` for subdomains, `rdap` for
registration, `dns` for the live records, then `ip` + `asn` on the resolved
address to see who hosts it. The `profile` subcommand chains exactly this
sequence for you (respecting ip-api's rate limit between enrichment calls) and
returns one merged JSON/`--human` report.
## Behavior & safety notes
- **Passive only.** No port scans, no direct connections to the target — just
public database lookups.
- **Keyless.** No credentials are ever required, read, or transmitted.
- **No shell injection surface.** Inputs are validated (domains IDNA/punycode-
encoded, IPs and ASNs parsed) and passed only as URL-encoded query parameters;
the helper never invokes a shell.
- **Courteous.** Sends a descriptive User-Agent, uses per-request timeouts, and
backs off with retries on `429`/`5xx` (honoring `Retry-After`). ip-api.com is
rate-limited to 45 requests/minute — batch and space out calls.
- **Resilient to source outages.** `certs` falls back from crt.sh (which
frequently hard-`502`s under load) to certSpotter so subdomain enumeration
survives, recording which source answered in `sources_used`. `rdap` resolves
the authoritative RDAP server via the **IANA bootstrap**, plus a small curated
**supplement** for TLDs IANA omits but that still run RDAP (e.g. `.io`, which
the rdap.org redirector `404`s), then falls back to rdap.org for anything else.
The winning source is recorded in `rdap_source`.
- **Graceful degradation — `rdap` tells you when a TLD isn't supported.** `rdap`
is the only TLD-variable query (dns/certs/ip/asn/wayback are TLD-agnostic).
When a domain's TLD has no public RDAP server anywhere (absent from both the
IANA bootstrap and the supplement — many ccTLDs are WHOIS-only), `rdap` returns
a first-class **`{"supported": false, "rdap_source": "none", "reason": ...}`**
result at exit `0`, *not* a 404 or an exception. A calling agent should check
`supported` and skip RDAP for that domain while still using the other sources;
inside `profile` this appears as a clean signal under `apex.rdap`, never in
`errors[]`. Real answers carry `"supported": true`. See
`docs/tld-rdap-coverage.md` for the full per-TLD map (which of the 1,438
delegated TLDs are `bootstrap` / `supplement` / `none`), regenerable with
`python3 {baseDir}/scripts/gen_coverage.py`.
- **Third state — `rdap` distinguishes "unreachable" from "unsupported".** A TLD
can be *delegated in the IANA bootstrap yet unreachable from our egress*: the
registry's authoritative RDAP server resets the connection / RSTs the TLS
handshake / read-times-out (errno 104 class). This is neither a clean answer
nor a real capability gap, so `rdap` emits a distinct
**`{"supported": true, "rdap_source": "unreachable", "retryable": true, "reason": ...}`**
result. The signal to a caller: *the data exists, we could not fetch it — skip
the field but know it is retryable*, NOT "this TLD has no RDAP server". The
trigger is deliberately narrow (connection reset / TLS RST / read timeout
against a bootstrap- or supplement-listed endpoint); a `429`/ban or a genuine
absence never lands here. Because the fetch did not succeed, this outcome
carries its **own exit code `3`** (see Exit codes) rather than collapsing into
`0`. Inside `profile` it appears as a clean `unreachable` signal under
`apex.rdap`, never in `errors[]`.
- **Fourth state — `rdap` distinguishes "broken" from "unreachable".** A
delegated endpoint can *respond, but with an unusable response*: an
untrusted/self-signed TLS certificate that fails verification, or an HTTP
error status (4xx / 5xx). This is neither a clean answer, a real capability
gap, nor a transport reset, so `rdap` emits a distinct
**`{"supported": true, "rdap_source": "broken", "cause": "<slug>", "http_status": <int|null>, "retryable": <bool>, "reason": ...}`**
result. `cause` is `bad-cert` or `http-<status>` (e.g. `http-426`, `http-404`,
`http-500`); `retryable` is `false` for a bad cert / 4xx (a persistent
registry-side fault) and `true` for a 5xx (a server error a later attempt may
clear). The signal to a caller: *the endpoint is faulty — skip the field;
`retryable` says whether trying later helps.* recon does **not** disable TLS
verification or fake a fetch — it only *labels* the fault honestly. The
trigger is narrow and mutually-exclusive with `unreachable`: a `429`/ban, a
DNS-resolution failure, a connection-refused, and a genuine absence never land
here, and `broken` is only emitted after **both** the authoritative server and
the rdap.org redirector are exhausted (so it never fires where the redirector
could still rescue the lookup). Exit code is driven by `retryable` (see Exit
codes). Inside `profile` it appears as a clean `broken` signal under
`apex.rdap`, never in `errors[]`.
- **`profile` runtime is bounded, not instant.** A full profile fans out across
~6 sources and multiple hosts with courtesy rate-limit spacing, so a large
domain (dozens of subdomains) takes roughly **1–1.5 min** — it is working, not
hung. The slowest source, the Wayback CDX index (single lookups seen in the
tens of seconds), runs on a deliberately tight budget (short timeout, no
retry) so it can never dominate; a timed-out archive lookup is fault-isolated
into `errors[]` like any other. Use `--no-wayback` or a lower `--ip-limit` to
make a profile faster.
- **Exit codes (quint-state).** The code encodes the *actionable retryability
tier*; the JSON `rdap_source` names the mechanism. A caller branching on exit
status can tell "clean result" from "skip, retry later" from "skip, don't
bother":
- `0` — success: a real answer **or** a definitive `unsupported`/WHOIS-only TLD
(a terminal, non-retryable capability gap, `rdap_source:"none"`).
- `2` — a handled error (bad input, upstream failure, **429/ban**) with a
message on stderr.
- `3` — **retryable** RDAP failure: the endpoint is delegated (a server exists)
but we could not get usable data this time and a later attempt may succeed —
either `rdap_source:"unreachable"` (transport reset / TLS-RST / read timeout)
or `rdap_source:"broken"` with a 5xx (`retryable:true`).
- `4` — **non-retryable** endpoint fault: `rdap_source:"broken"` with an
untrusted/self-signed TLS cert or an HTTP 4xx (`retryable:false`) — the
server answered definitively; retrying will not help until the registry fixes
it.
Neither `3` nor `4` is folded into `0`: the field was not fetched, so the
distinction (retry later vs. don't bother vs. clean) is preserved.
## For maintainers — running the tests
The parsing/normalization logic is covered by an offline, deterministic pytest
suite (fixtures in `scripts/tests/fixtures/`, HTTP mocked — no network):
```bash
python3 -m pip install pytest # only dependency, tests-only
python3 -m pytest {baseDir}/scripts/tests/test_recon.py -q
```
An opt-in live smoke test verifies the real endpoints still match the expected
schema (never part of the normal gate):
```bash
RECON_LIVE=1 python3 -m pytest {baseDir}/scripts/tests/test_live_smoke.py -q
```
See `references/API_NOTES.md` for endpoint details and `references/CHANGELOG.md`
for notable decisions (including the BGPView → RIPEstat swap).
don't have the plugin yet? install it then click "run inline in claude" again.
extracted implicit decision logic (RDAP TLD fallback chain, source auto-retry, certs dual-source fallback, exit code semantics) into explicit decision points; documented all five external API connections with rate limits and failure modes; spelled out the four RDAP outcomes (supported/unsupported/unreachable/broken) with their distinct exit codes; added edge cases (network timeouts, TLS verification, rate limits, empty result handling) to procedure; clarified output contract with concrete JSON schema examples and file locations; expanded outcome signal to cover all exit codes and result interpretation.
Perform passive reconnaissance on a domain, IP address, or ASN using five keyless public APIs. The skill queries Certificate Transparency logs for subdomains, RDAP/WHOIS for registration data, DNS-over-HTTPS for live records, ip-api for geolocation and ISP info, and RIPEstat for network ownership. Use this when you need to map a target's public footprint: enumerate subdomains, resolve DNS, locate IP geography, or identify which network owns an address. All queries are passive (no direct connection to the target, no probing) and require no API keys or credentials.
Required:
External connections (keyless, no auth required):
--provider cloudflareOptional parameters (all passed to python3 recon.py):
--human - return readable text instead of JSON--timeout <sec> - per-request timeout in seconds (default 20)--retries <n> - retry attempts on 429/5xx/network errors (default 3)Validate and parse the target input. Convert the domain to IDNA/punycode encoding if international characters present. Validate IPv4/IPv6 format. Validate ASN format (AS + digits). Auto-detect resource type (domain/ip/asn) for multi-purpose subcommands. Output: normalized target string and detected type.
Run the appropriate subcommand via python3 {baseDir}/scripts/recon.py <subcommand> <target> [options]:
certs <domain> - enumerate subdomains from Certificate Transparency logs and cert historyrdap <resource> - query WHOIS/RDAP registration data (auto-detects domain/ip/asn)dns <name> - resolve DNS records (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA); supports multiple types per call via --type A,AAAA,MXip <ip> - geolocate IP, retrieve ISP, ASN, and proxy/hosting flagsasn <resource> - retrieve ASN holder and announced prefixes; or IP-to-ASN reverse lookupwayback <url> - check Wayback Machine availability and CDX capture historyprofile <domain> - orchestrate all sources (certs → dns → ip → asn → wayback) in one call--human formatted text summary.Respect rate limits and timeouts during execution. The skill auto-retries on 429 (rate limit), 5xx errors, and network timeouts up to --retries times. ip-api.com is capped at 45 req/min globally, so calls are automatically spaced. Per-request timeout is --timeout seconds (default 20); exceeding it triggers a retry or failure. Output: JSON includes sources_used and rdap_source fields to track which endpoint answered.
Handle RDAP TLD coverage gracefully. Query the IANA bootstrap and supplement lists to determine if a domain's TLD has a public RDAP server. If TLD is not in the bootstrap or supplement, check rdap.org fallback. If no RDAP server exists for that TLD, return a clean {"supported": false, "rdap_source": "none", "reason": "..."} result at exit code 0 (not an error). Output: supported boolean and rdap_source field name the outcome; caller should check supported before trusting RDAP data.
Distinguish RDAP reachability and server faults from missing TLD support. If a TLD's delegated RDAP endpoint is reachable but transport fails (connection reset, TLS RST, read timeout), emit {"supported": true, "rdap_source": "unreachable", "retryable": true, "reason": "..."} at exit code 3 (retryable). If the endpoint responds with an HTTP error or untrusted certificate, emit {"supported": true, "rdap_source": "broken", "cause": "bad-cert|http-<status>", "retryable": <bool>, "reason": "..."} at exit code 3 (5xx, retryable) or 4 (4xx/bad-cert, non-retryable). Output: caller sees the specific failure mode and whether to retry later.
Collect results and merge for profile orchestration. If using profile, the skill chains certs → rdap → dns → ip → asn → wayback, respecting rate limits between calls (especially for ip-api). All results are merged into a single JSON report keyed by stage (e.g. apex.certs, apex.rdap, apex.dns, apex.ip, apex.asn, apex.wayback). Errors are isolated to errors[] array, not merged into clean fields; e.g. a timed-out archive lookup goes to errors[], not apex.wayback. Output: one merged JSON/--human report; runtime roughly 1-1.5 min for a large domain (dozens of subdomains) due to courtesy rate-limit spacing.
Return exit code reflecting actionable retryability. Exit 0 on clean success or definitive unsupported TLD. Exit 2 on handled errors (bad input, upstream failure, 429 ban). Exit 3 on retryable RDAP failures (unreachable or broken with 5xx). Exit 4 on non-retryable RDAP faults (broken with untrusted cert or 4xx). Output: caller branches on exit status to decide skip/retry/fail behavior without parsing JSON.
If user asks for subdomains: Run certs <domain> (primary). If crt.sh returns 502 or timeout, automatically fall back to certSpotter and record the switch in sources_used. If user adds --all-ct, query both sources and merge for widest coverage instead of stopping at the first success.
If user wants full domain profile: Run profile <domain> instead of manual chaining. If user prefers to skip archive lookups (to speed up), add --no-wayback. If user wants to cap subdomain or IP enumeration to save time, add --cert-limit <n> or --ip-limit <n>.
If user queries a domain's RDAP/WHOIS data: Auto-detect the TLD. If TLD is in IANA bootstrap or the curated supplement, fetch from the authoritative server. If not in either list, try rdap.org redirector as fallback. If rdap.org 404s or the authoritative server is unreachable/broken, return a clean supported:false or unreachable/broken result (not an exception); caller should skip that domain or retry based on the signal.
If user queries an IP or ASN's RDAP data: Auto-detect the resource type and query the appropriate authoritative server. RDAP for IP addresses and ASNs is more stable than domain RDAP, so fallback to rdap.org is still offered but failure is rarer.
If user specifies DNS record types: Pass --type A,AAAA,MX to query multiple types in one call. If no --type is given, default to A records.
If user prefers Cloudflare DoH over Google: Add --provider cloudflare to the dns call; default is Google.
If user wants human-readable output: Add --human to any subcommand; default is JSON.
If user hits rate limits on ip-api.com (45 req/min): The skill auto-retries with backoff on 429. If limit is persistently hit (e.g. many parallel calls), slow down or use --retries 0 to fail fast instead of waiting.
If a network request times out: The skill retries up to --retries times (default 3). If all retries exhaust the per-request timeout (default 20 sec), the call fails and the result is recorded in errors[] (or omitted from the main result).
If a TLS certificate cannot be verified during RDAP fetch: The skill does not disable verification or fake a successful fetch. Instead, it reports rdap_source:"broken" with cause:"bad-cert" and retryable:false (a persistent registry-side fault). Caller should not retry.
Default (JSON): Each subcommand returns a JSON object with fields specific to the query type:
certs → {"subdomains": [list], "cert_count": int, "sources_used": [list], "error": null|string}rdap → {"supported": bool, "rdap_source": "bootstrap|supplement|rdap.org|none|unreachable|broken", "registrar": string, "registered": date, "updated": date, "expires": date, "name_servers": [list], "retryable": bool|null, "cause": string|null, "reason": string|null, "http_status": int|null, "error": null|string}dns → {"records": {type: [answers]}, "authoritative": [list], "error": null|string}ip → {"country": string, "region": string, "city": string, "latitude": float, "longitude": float, "isp": string, "asn": string, "org": string, "proxy": bool, "hosting": bool, "error": null|string}asn → {"holder": string, "prefixes": [list], "announced": int, "error": null|string}wayback → {"available": bool, "first_capture": date|null, "last_capture": date|null, "capture_count": int, "recent": [list], "error": null|string}profile → merged object with keys apex (target domain RDAP/IP/ASN) and subdomains (array of each subdomain's dns/ip/asn), plus errors array for fault-isolated failures and runtime_sec for total elapsed time.Human-readable (--human):
Compact text summary, one result per line or a formatted table. Example: Domain: example.com | Subdomains: 42 | IP: 1.2.3.4 | ISP: Example Inc | Country: US.
File location:
Output writes to stdout. Caller can redirect to file (e.g. ... > report.json) or parse live.
Exit codes:
0 , success or definitive unsupported TLD2 , handled error (bad input, 429 ban, upstream failure)3 , retryable RDAP fault (unreachable or broken with 5xx)4 , non-retryable RDAP fault (broken with untrusted cert or 4xx)Check exit code first. Exit 0 means a clean result or a definitive "no RDAP for this TLD". Exit 2 means a real error (check stderr for message). Exit 3 or 4 means RDAP failed; caller should check rdap_source in the JSON and retryable to decide whether to retry.
Parse JSON for the specific data you need. For certs, check subdomains array length. For rdap, check supported first; if true, read registration dates and name servers. For dns, check records for the query type (e.g. records.A). For ip, check country, isp, hosting flags. For asn, check holder and prefixes list. For profile, iterate subdomains and check each stage's result in apex.
Look for sources_used or rdap_source to confirm which upstream answered. If certs came from certSpotter instead of crt.sh, sources_used will list certSpotter. If RDAP came from rdap.org instead of the authoritative server, rdap_source will show rdap.org. This tells you the reliability/freshness of the data.
Check the errors array in profile output. If any stage timed out or failed, it appears in errors with a reason; the result is not collapsed into the main fields, so you can safely use the stages that did succeed while knowing which ones need retry or manual follow-up.
Watch runtime for profile calls. If a profile takes 1-1.5 min and you see no errors, that is normal (not a hang). The Wayback CDX index is especially slow (tens of seconds for large result sets); use --no-wayback or a lower --ip-limit to speed up if you do not need archive data.
Spot rate-limit hits on ip-api.com. If you see HTTP 429 in stderr or the result repeats a retry message, ip-api is rate-limited. Space out calls or use batch mode with --retries 3 to auto-backoff; do not hammer the endpoint in parallel.