Command Line Options
Output of urx --help:
Usage: urx [OPTIONS] [DOMAINS]... [COMMAND]
Commands:
cache Inspect and maintain the URL cache (stats, list, prune, drop, clear)
help Print this message or the help of the given subcommand(s)
Arguments:
[DOMAINS]... Domains to fetch URLs for
Options:
-c, --config <CONFIG> Config file to load
--provider-config <PATH> Separate provider config holding only API keys (default: ~/.config/urx/provider-config.toml; %APPDATA%\urx\provider-config.toml on Windows)
--completions <SHELL> Print a shell completion script (bash, zsh, fish, powershell, elvish) to stdout and exit
--manpage Print the roff man page to stdout and exit
-h, --help Print help
-V, --version Print version
Input Options:
--files <FILES>... Read URLs directly from files (supports WARC, URLTeam compressed, and text files)
--domain-list <PATH> File of newline-separated domains to scan (repeatable; merged with positional DOMAINS; stdin is read only when they name no domains; `#` comments allowed) [alias: --dL]
Output Options:
-o, --output <OUTPUT> Output file to write results
--output-dir <PATH> Write one file per URL host into this directory; extension matches --format. Coexists with --output / stdout. [alias: --oD]
-f, --format <FORMAT> Output format: "plain", "json", "jsonl", "csv", "wordlist" (case-insensitive; anything else is a usage error) [default: plain]
--merge-endpoint Merge endpoints with the same path and merge URL parameters
--stream Write URLs as providers report them (plain/jsonl/csv only; unsorted; bypasses cache)
--normalize-url Normalize URLs for better deduplication
--dedup-similar Collapse URLs differing only in ids, hashes, dates, or query values
--params Replace the URL list with every query parameter name the run saw, once each
--params-by-endpoint One line per endpoint: the endpoint and the union of the parameter names seen on it
--fuzz-placeholder <VALUE> Replace every query parameter value with VALUE, one URL per parameter signature
Provider Options:
--providers <PROVIDERS> Providers to use (comma-separated) [default: wayback,cc,otx]
--exclude-providers <PROVIDERS> Providers to exclude (wins on conflict)
--all-providers Enable every supported provider (API-keyed ones only if a key is available)
--list-providers List every supported provider then exit
--subs Include subdomains when searching
--cc-index <CC_INDEX> Common Crawl index(es), comma-separated for parallel queries; `latest` auto-resolves [default: latest]
--cdx-endpoint <URL> Query an additional CDX index server (pywb / OutbackCDX / classic) by its API URL; repeatable; id `cdx:<host>` (`cdx:<host>:<port>` with a port)
--cdx-dialect <DIALECT> Dialect of the --cdx-endpoint servers: `pywb` or `classic` (unset: probed once, pywb fallback)
--from <DATE> Restrict CDX providers to captures >= DATE (YYYY/YYYYMM/YYYYMMDD/YYYYMMDDhhmmss); legacy alias --wayback-from
--to <DATE> Restrict CDX providers to captures <= DATE (same format as --from); legacy alias --wayback-to
--archive-status <CODE> Keep only captures the archive recorded with this status code (a list works on wayback/classic endpoints only)
--archive-exclude-status <CODES> Drop captures the archive recorded with these status codes
--archive-mime <TYPE> Keep only captures with this recorded MIME type (a list works on wayback/classic endpoints only)
--archive-exclude-mime <TYPES> Drop captures with these recorded MIME types
--vt-api-key <VT_API_KEY> API key for VirusTotal
--urlscan-api-key <URLSCAN_API_KEY> Optional API key for Urlscan (also works anonymously)
--zoomeye-api-key <ZOOMEYE_API_KEY> API key for ZoomEye
--github-api-key <GITHUB_API_KEY> Personal access token for GitHub Code Search (URX_GITHUB_API_KEY)
--bevigil-api-key <BEVIGIL_API_KEY> API key for BeVigil, URLs from unpacked Android apps (URX_BEVIGIL_API_KEY)
Discovery Options:
--exclude-robots Exclude robots.txt discovery
--exclude-sitemap Exclude sitemap.xml discovery
--archived-discovery Also read every distinct archived version of robots.txt and sitemap.xml (see "Archived robots.txt and sitemap.xml" below)
--archived-discovery-limit <N> Maximum archived documents fetched per domain by each archived provider; nested sitemaps count [default: 50]
Display Options:
-v, --verbose Show verbose output
--silent Silent mode (no output)
--no-progress No progress bar
--no-color Disable ANSI color (NO_COLOR is also honored)
--show-sources Annotate output URLs with the providers that returned them
--show-meta Annotate plain-text URLs with the archive capture metadata
--stats Print a per-provider summary to stderr at end of run
Filter Options:
-p, --preset <PRESET> Filter Presets (e.g., "no-resources,no-images,only-js,only-secrets,only-api")
-e, --extensions <EXTENSIONS> Filter by extensions (e.g., "js,php,aspx")
--exclude-extensions <EXTENSIONS> Exclude extensions (e.g., "html,txt")
--patterns <PATTERNS> Include URLs containing patterns
--exclude-patterns <PATTERNS> Exclude URLs containing patterns
--match-regex <RE> Keep only URLs matching this regex (repeatable, ORed, case-sensitive)
--filter-regex <RE> Drop URLs matching this regex (repeatable; one match is enough)
--show-only-host Only show the host part
--show-only-path Only show the path part
--show-only-param Only show the parameters part
--min-length <MIN_LENGTH> Minimum URL length
--max-length <MAX_LENGTH> Maximum URL length
--strict Enforce exact host validation (default)
--no-strict Disable host validation; a target's path scope still applies (wins over --strict)
--scope-file <FILE> Bug-bounty host scope file (`!` excludes, `*.host` wildcard, `[2001:db8::1]` exact IPv6); repeatable
--meta-first-seen-after <DATE> Keep URLs whose oldest archived capture is on or after DATE
--meta-first-seen-before <DATE> Keep URLs whose oldest archived capture is on or before DATE
--meta-last-seen-after <DATE> Keep URLs whose newest archived capture is on or after DATE ("still alive as of")
--meta-last-seen-before <DATE> Keep URLs whose newest archived capture is on or before DATE ("dead since")
--meta-mime <TYPE> Keep only URLs with these archived MIME types (`image/*` matches any subtype)
--meta-exclude-mime <TYPE> Drop URLs with these archived MIME types
--meta-status <CODE> Keep only URLs with these archived status codes (20x / 5xx patterns)
--meta-exclude-status <CODE> Drop URLs with these archived status codes
Network Options:
--network-scope <SCOPE> Apply settings to: all, providers, testers, providers,testers [default: all]
--proxy <PROXY> HTTP proxy (e.g., http://proxy:8080)
--proxy-auth <PROXY_AUTH> Proxy credentials (username:password)
--insecure Skip SSL certificate verification
--random-agent Use a random User-Agent
-H, --header <NAME: VALUE> Extra request header, repeatable; sent to the target only, never to an archive
--cookie <COOKIES> Cookie header for requests to the target
--user-agent <STRING> User-Agent for requests to the target, overriding --random-agent
--timeout <TIMEOUT> Request timeout in seconds [default: 120]
--retries <RETRIES> Retries for failed requests [default: 2]
--parallel <PARALLEL> Max domains fetched concurrently per provider, and concurrent URL tests (rate-limit shared) [default: 5]
--rate-limit <RATE_LIMIT> Requests per second
--rate-limit-by <PAIRS> Per-provider rate overrides (e.g. `vt=1,wayback=10`); falls back to --rate-limit for unlisted providers
--max-time <SECONDS> Global ceiling on provider enumeration time in seconds; in-flight fetches are aborted at deadline (0 = unlimited) [default: 0]
Testing Options:
--check-status Check HTTP status code of collected URLs [alias: --cs]
--include-status <INCLUDE_STATUS> Include specific status codes (e.g., 200,30x); implies the status check [alias: --is]
--exclude-status <EXCLUDE_STATUS> Exclude specific status codes (e.g., 404,50x); implies the status check [alias: --es]
--extract-links Extract additional links from collected URLs (see "Link Extraction" below)
--extract-js-endpoints Fetch collected JavaScript and extract the endpoints in its string literals; with --archive-body also mines archived scripts (see "JavaScript Endpoint Extraction" below)
--max-js-files <N> Maximum number of files --extract-js-endpoints will fetch (0 = unlimited) [default: 500]
--archive-body Extract links from the *archived* body of each collected URL (see "Archived Response Bodies" below)
--archive-body-limit <N> Maximum archived bodies fetched per run; bounds distinct bodies, not URLs [default: 500]
--archive-body-dir <DIR> Keep every replayed body in DIR with an index.jsonl mapping it back to its URL
--expand-specs Fetch collected OpenAPI/Swagger/GraphQL documents and expand every route they describe (see "API Specification Expansion" below)
--max-spec-files <N> Maximum number of specification documents --expand-specs will fetch (0 = unlimited) [default: 50]
--check-title Also record each response's HTML <title>; implies --check-status
Cache Options:
--incremental Only return new URLs compared to previous scans
--cache-type <CACHE_TYPE> Cache backend: sqlite or redis [default: sqlite]
--cache-path <CACHE_PATH> Path for SQLite cache database
--redis-url <REDIS_URL> Redis connection URL
--cache-ttl <CACHE_TTL> Cache TTL in seconds [default: 86400]
--no-cache Disable caching entirely
Notification Options:
--notify <URL> POST a run summary to this webhook when the run ends (repeatable; also URX_NOTIFY_URL, provider-config `notify_url`, or `[notify].url`)
--notify-on <NOTIFY_ON> When to send: new (only if URLs were emitted), always, or never [default: new]
--notify-format <NOTIFY_FORMAT> Payload shape: json (urx summary), slack ({"text"}), or discord ({"content"}) [default: json]
urx cache is the only subcommand. See
Inspecting and Maintaining the Cache.
Everything else on this page applies to a scan.
Webhook Notifications
--notify <URL> POSTs a run summary to a webhook when the run ends. Run it
from cron with --incremental and it fires only when the archives have
something new.
# slack, only when new URLs turned up (default --notify-on new)
urx target.com --incremental --silent \
--notify https://hooks.slack.com/services/T000/B000/XXXX --notify-format slack
# discord, every run
urx target.com --incremental --notify "$DISCORD_HOOK" --notify-format discord --notify-on always
# several receivers, urx JSON schema
urx target.com --incremental --notify https://n8n.example/hook --notify https://ntfy.example/urx
# keep the webhook out of shell history
export URX_NOTIFY_URL=https://hooks.slack.com/services/...
urx target.com --incremental --notify-format slack
--silent still sends, but it also hides the URL list on stdout. Add -o to
keep the results.
When it sends
--notify-on |
Sends |
|---|---|
new (default) |
Only when the run emitted at least one URL. Under --incremental, at least one URL the previous run had not seen. |
always |
After every run, including one with zero URLs. |
never |
Nothing. The configuration stays in place. |
Payload formats
json (default) uses urx's own schema:
{
"tool": "urx",
"version": "0.11.0",
"domains": ["example.com"],
"incremental": true,
"url_count": 12,
"new_url_count": 12,
"elapsed_ms": 3210,
"providers": [
{"name": "Wayback Machine", "urls": 1200, "errors": 0, "partial": 0, "elapsed_ms": 2500, "aborted": false}
],
"sample": ["https://example.com/api/v2/users", "..."],
"sample_truncated": false
}
incremental is true only when the run diffed against the cache, so it is
false under --no-cache or --files. providers has the same numbers
--stats prints. sample holds up to 20
emitted URLs in output order, and sample_truncated is true when there were
more. Under --stream the payload has the count and an empty sample
(sample_truncated is true if any URL was emitted).
slack sends {"text": "..."} and discord sends {"content": "..."}. The
message is a header line (count, targets, elapsed time), one line of provider
totals, the URL sample, and … N more not shown when the sample is short of
the total. The text is cut on a line boundary at 2000 characters for Discord
and 4000 for Slack, and ends with
[truncated: N lines cut to fit the message limit]. A URL is never split.
Archived URLs cannot ping anyone: Slack text escapes &, < and >, and the
Discord payload sets "allowed_mentions": {"parse": []}.
Failure handling
Delivery never changes the exit code. The URLs are already on stdout or in
--output, so a webhook that is down, slow or returns 4xx/5xx prints a warning
on stderr and the run exits 0. --verbose prints the HTTP status of each
delivery; --silent hides those lines but still sends. Each URL is tried once
with no retry: chat webhooks are not idempotent, and a retry after a slow but
delivered request would post twice.
The URL is a secret
A Slack or Discord webhook URL is the credential. urx prints only its scheme,
host and non-default port (https://hooks.slack.com) in verbose output,
warnings, --stats and error text, and the payload never contains it.
To keep it out of a checked-in config, use the URX_NOTIFY_URL environment
variable or notify_url in the provider-config file. [notify].url in the main
config also works. Precedence is CLI/env > provider-config > main config, the
same as API keys. The [notify] section also takes on and format (the
config equivalents of --notify-on and --notify-format).
Network settings
The request uses --proxy, --proxy-auth, --timeout and --insecure.
--network-scope does not apply: it splits traffic between the archives
(providers) and the target (testers), and the webhook is your own endpoint.
Archive Capture Metadata
The CDX-backed providers (wayback, cc, arquivo, and any --cdx-endpoint)
return captures, so each URL comes with these fields:
| Field | Meaning |
|---|---|
first_seen |
Oldest capture timestamp, 14-digit CDX form (YYYYMMDDhhmmss) |
last_seen |
Newest capture timestamp |
mime |
MIME type of the most recent capture that recorded one |
archive_status |
HTTP status the archive recorded at capture time |
digest |
A representative content digest across the captures |
archive_status is what the crawler got at capture time. status (only under
--check-status) comes from requesting the URL live now. A URL can have
archive_status 200 and be dead today.
When the same URL comes from several captures or archives, the fields merge:
first_seen is the oldest timestamp, last_seen the newest, and
mime/archive_status come from the most recent capture that had them.
Wayback returns one capture per URL, its newest, so from Wayback alone
first_seen equals last_seen.
Providers with no capture index (otx, vt, urlscan, zoomeye, github,
bevigil, robots, sitemap) and --files input report the URL only. A
domain served from cache has no metadata either (the cache stores URLs only);
use --no-cache to get it back.
| Format | Output |
|---|---|
json / jsonl |
A key per field, only when it has a value |
csv |
A column per field, only when at least one row has a value |
plain |
Bare URL by default. --show-meta appends [first_seen=… last_seen=… mime=…] |
A URL rewritten by --normalize-url or --merge-endpoint no longer matches
what the providers reported, so it has no sources or capture metadata.
Source Attribution (--show-sources)
--show-sources names each source by its run label (the name --stats
prints) rather than its --providers id: Wayback Machine, CC (latest) or
the pinned index name (e.g. CC-MAIN-2026-17), OTX, Arquivo.pt,
VirusTotal, Urlscan, ZoomEye, GitHub, BeVigil, Robots.txt,
Sitemap, Robots.txt (archived), Sitemap (archived), cdx:<host>, and
file for --files input.
JSON gives a sources array, plain text a [a,b] suffix, and CSV one
sources cell joined with | (e.g. CC (latest)|Wayback Machine). URLs
served from cache, or found by --extract-links, --extract-js-endpoints,
--archive-body or --expand-specs, have no sources.
--show-sources and --show-meta are rejected under --stream. A streamed URL
is printed on first sighting, before other providers or later captures could
add to it.
urx example.com --providers wayback -f jsonl
# {"url":"https://example.com/old.php","first_seen":"20040112093000",
# "last_seen":"20180722140311","mime":"text/html","archive_status":"200",
# "digest":"HT2DYGA5UKZCPBSFVCV3JOBXGW2G5UUA"}
# everything last captured before 2010
urx example.com -f jsonl | jq -r 'select(.last_seen and .last_seen < "20100101000000") | .url'
# metadata in plain output
urx example.com --providers wayback --show-meta
Live Response Metadata
--check-status records these fields from the response head along with the
status:
| Field | Meaning |
|---|---|
status |
Code and reason (200 OK), or Status check failed when the request failed (connection refused, timeout, TLS). Failed URLs are kept unless --include-status is set |
location |
The Location header of a 3xx, recorded but never followed |
content_length |
The Content-Length header, verbatim |
content_type |
The Content-Type header, verbatim |
title |
The HTML <title>, only under --check-title |
- Redirects are never followed, so
statusalways belongs to the requested URL andlocationis where the 3xx pointed. - With both
--include-statusand--exclude-status, only--include-statusapplies. Use one or the other. --check-titleimplies--check-statusand has to read the body. It reads at most 64 KiB (checked between chunks), stops at the closing tag, and skips bodies the server declared as non-HTML. The title is whitespace-collapsed, entity-decoded and cut to 200 characters (with a trailing…when longer).
| Format | Output |
|---|---|
json / jsonl |
A key per field, only when it has a value |
csv |
Columns appended after the existing ones. The title gets the same spreadsheet-formula escaping as the URL |
plain |
The URL and [status]. --check-title appends [title="…"] (quoted, titles contain spaces); --show-meta adds location, content_length and content_type to the same bracket |
# status plus response head, as JSON Lines
urx example.com --check-status -f jsonl
# titles in plain text (add --show-meta for the head fields)
urx example.com --check-title
# where did the redirects point?
urx example.com --check-status --is 30x -f jsonl | jq -r '.url + " -> " + .location'
Available Providers
| Provider | Flag | API Key Required | Environment Variable |
|---|---|---|---|
| Wayback Machine | wayback |
No | - |
| Common Crawl | cc |
No | - |
| OTX (AlienVault) | otx |
No | - |
| Arquivo.pt | arquivo |
No | - |
| VirusTotal | vt |
Yes | URX_VT_API_KEY |
| URLScan | urlscan |
No (optional) | URX_URLSCAN_API_KEY |
| ZoomEye | zoomeye |
Yes | URX_ZOOMEYE_API_KEY |
| GitHub Code Search | github |
Yes | URX_GITHUB_API_KEY |
| BeVigil | bevigil |
Yes | URX_BEVIGIL_API_KEY |
| robots.txt discovery | robots |
No | - |
| sitemap.xml discovery | sitemap |
No | - |
| Custom CDX server | cdx:<host> (via --cdx-endpoint URL) |
No | - |
Defaults are wayback,cc,otx, plus robots and sitemap, which fetch the
target's live robots.txt and sitemap.xml. Turn those two off with
--exclude-robots, --exclude-sitemap or --exclude-providers robots,sitemap
(--all-providers still honours those switches).
- Keyed providers are enabled automatically when their key is set.
arquivo(the Portuguese web archive) is keyless but opt-in. Add it with--providersor--all-providers.urlscanworks without a key (about 30 requests/min per IP). A key raises the limits and enables rotation.githubsearches GitHub Code Search and needs a personal access token (--github-api-keyorURX_GITHUB_API_KEY).bevigilreturns URLs BeVigil extracted from unpacked Android apps, and needs an API key (--bevigil-api-keyorURX_BEVIGIL_API_KEY).
urx --list-providers prints the catalog (id, key requirement, one-line
summary) from the binary. It also lists any --cdx-endpoint named on the same
command line, with the id it will run as.
Custom CDX Endpoints
--cdx-endpoint URL queries any pywb, OutbackCDX or Internet Archive-style CDX
server:
# Icelandic web archive, alongside the default providers
urx example.is --cdx-endpoint https://vefsafn.is/cdx
# only that endpoint, one request per second
urx example.is --cdx-endpoint https://vefsafn.is/cdx --providers cdx:vefsafn.is --rate-limit-by cdx:vefsafn.is=1
# several at once; each gets its own progress line, stats row and rate limit
urx example.com --cdx-endpoint https://vefsafn.is/cdx --cdx-endpoint http://localhost:8080/cdx
- The provider id is
cdx:<host>, orcdx:<host>:<port>when the URL has a port. Use it with--providers,--exclude-providersand--rate-limit-by;--statsand--show-sourcesshow it. Naming an endpoint enables it. - It honours
--subs,--from/--to, the--archive-*filters, pagination and rate limiting likewayback/cc/arquivo, and reports capture metadata. --cdx-dialect classic|pywbsets the server's dialect (field names, filter semantics, row format, pagination; see Archive-side Filtering). Unset, urx probes once per run and falls back topywb. Set it when the probe can't tell, e.g. the server gives an empty answer for an unknown domain.- Config file:
cdx_endpoint = [...]andcdx_dialectunder[provider].
The only public endpoint verified to work is https://vefsafn.is/cdx
(Iceland, pywb). It ignores limit, page and showNumPages and returns the
whole result set, which urx handles. After a few requests it may answer with an
Anubis-style "Session Verification" page. urx reports that as an error naming
the endpoint, never as an empty result. Slow down with
--rate-limit-by cdx:vefsafn.is=1 or retry later.
These do not work: the UK Web Archive (webarchive.org.uk), the Library of
Congress (webarchive.loc.gov), Bibliotheca Alexandrina and the National
Library of Australia (web.archive.org.au). Bot protection or redirects block
their CDX APIs, and urx reports the same HTML-answer error.
Provider Notes
| Provider | What it queries, and its limits |
|---|---|
otx |
/api/v1/indicators/{domain|hostname}/…/url_list, 200 URLs per page, at most 1,000 pages. The hostname endpoint is used for a target with three or more labels (sub.example.com, but also example.co.uk) without --subs; it excludes www. and other subdomains |
vt |
v3 domains/{domain}/urls, 40 per page with cursor pagination. A 404 means no data; a 429 waits for the server's Retry-After (at most 60 s) before retrying |
urlscan |
search/?q=page.domain:<domain> with 100 results per page and search_after paging. Works without a key |
github |
Code Search for "<domain>"; URLs come from the matched text fragments, not whole files. GitHub caps code search at 1,000 results (10 pages × 100). Any personal access token works |
zoomeye |
api.zoomeye.ai/v2/search with the dork site:<domain> (site:*.<domain> under --subs), 100 per page |
bevigil |
One request per domain to osint.bevigil.com/api/<domain>/urls/, no pagination |
--subs changes the query only for the CDX providers, ZoomEye
(site:*.<domain>), and OTX on a target with three or more labels (it switches
from the hostname to the domain endpoint). GitHub sends the same search
either way but keeps subdomain URLs only under --subs; without it, GitHub
keeps URLs on exactly the target host (not even www.). VirusTotal, urlscan,
BeVigil and the robots/sitemap fetchers send the same query either way, and
host validation keeps or drops the subdomains they return.
Naming a keyed provider without a key prints Error: The … provider (…) requires an API key… (not under --silent or --all-providers) and skips it. The run
continues with exit code 0 if any other provider remains, including the default
robots/sitemap probes. If it was the only provider, the run fails with
No valid providers specified and exits 1.
Reading URLs from Files
--files reads URLs from local files. No provider is queried.
- Positional domains and
--domain-listare ignored.--filestakes several paths, so a domain placed after it is read as another file. - No host validation, since there is no target. Use
--scope-fileor--match-regexto restrict hosts. - Every URL is attributed to
filein--show-sourcesand--stats. - Filters, testers and output options work as usual.
--streamis rejected; the cache and--incrementalare not used.
The format is picked by file name:
| File name | Read as |
|---|---|
.warc |
WARC |
.gz or .bz2 |
WARC if the name contains warc, otherwise URLTeam |
.txt or .list |
text |
other, containing warc |
WARC |
other, containing urlteam / url_team |
URLTeam |
| anything else | text |
WARC and URLTeam files are checked for gzip by magic bytes, and multi-member
streams are read in full. Text files are never decompressed: a gzip or bzip2
file with a text name yields no URLs and no error, so give it a .gz extension
or decompress it first. The WARC and URLTeam readers do not support bzip2: the
run stops with bzip2 input is not supported. Decompress it first
(bzip2 WARC input… for a WARC) and exits 1.
| Format | Extracted |
|---|---|
| text | Each non-blank, non-# line starting with http:// or https:// |
| URLTeam | The first http(s):// token on each line |
| WARC | WARC-Target-URI headers and bare URL lines in the payloads |
Each file is capped at 1,000,000 URLs and 1 GiB of decompressed input. Lines over 1 MiB are skipped. Hitting a cap prints a warning.
urx --files urls.txt --check-status --include-status 200
urx --files crawl.warc.gz --scope-file scope.txt -f jsonl
Shell Completions and the Man Page
Both are generated by the binary, so they match the installed version. Neither needs a target domain. Regenerate after upgrading urx.
# zsh: write into a directory on $fpath, then re-run compinit
urx --completions zsh > ~/.zfunc/_urx
# bash
urx --completions bash > ~/.local/share/bash-completion/completions/urx
# fish
urx --completions fish > ~/.config/fish/completions/urx.fish
# powershell
urx --completions powershell | Out-String | Invoke-Expression
# elvish
urx --completions elvish > ~/.config/elvish/lib/urx.elv
# man page
urx --manpage > ~/.local/share/man/man1/urx.1
man urx
Filter Presets
no-* presets exclude a family, only-* presets keep only that family.
Singular spellings (e.g. no-image, only-font) work too.
| Preset | Description |
|---|---|
no-resources |
Exclude resource files (JavaScript and its relatives such as .json / .map, stylesheets, images, fonts, documents, videos, audio) |
no-images |
Exclude image files |
no-fonts |
Exclude font files |
no-documents |
Exclude document files |
no-videos |
Exclude video files |
no-audio |
Exclude audio files |
only-js |
Only JavaScript and related sources (js, mjs, cjs, jsx, ts, tsx, vue, svelte, json, map, …) |
only-style |
Only stylesheet files |
only-fonts |
Only font files |
only-documents |
Only document files |
only-videos |
Only video files |
only-audio |
Only audio files |
only-images |
Only image files |
Security Presets
These match a listed extension or a listed shape. Substring shape rules are
checked against the lower-cased path, query and fragment (never the host). So only-secrets
catches /.env (no extension) and only-backup catches /index.php~.
| Preset | Description |
|---|---|
only-secrets |
Leaked credentials and VCS metadata: /.env, /.git/, /.svn/, /.aws/, /.ssh/, id_rsa, .htpasswd, credentials, *.pem, *.key, *.p12 |
only-backup |
Backups and archived copies: *.bak, *.old, *.orig, *.swp, *.sql, *.dump, *.zip, *.tar.gz, /backup/, and paths ending in ~ |
only-config |
Configuration files: *.conf, *.config, *.ini, *.yaml, *.yml, *.toml, *.properties, web.config, .htaccess, .npmrc, Dockerfile |
only-api |
API surfaces: /api/, /v1/ to /v4/, /rest/, /graphql, /swagger, /openapi, /wp-json, *.wsdl |
Singular and plural spellings both work (only-secret, only-backups,
only-configs, only-apis). Presets combine with OR, so
-p only-secrets,only-backup keeps what either one keeps.
urx example.com -p only-secrets
urx example.com -p only-backup,only-config
Scoping a Run to a Path
A target can include a path. urx example.com/shop collects only the part of
the site under /shop.
urx example.com/shop
urx https://example.com/api/v2 # a pasted URL works too
urx 'example.com/shop/*' # a trailing * is dropped: same scope
CDX providers get the path in the query (url=example.com/shop*), so the
archive never sends the rest of the site. On a large target that is a few
hundred rows instead of a few hundred thousand.
- Providers that can't put a path in their query (OTX, VirusTotal, urlscan,
GitHub, BeVigil, ZoomEye, and the
robots/sitemapfetchers, which always read the site root) are queried by host. Host validation then narrows their results. - With
--substhe path can't go in the query either: the leading*.selectsmatchType=domain, which no CDX server combines with a path. The prefix is applied to the results instead. - Scope means at or under the path:
/shopand/shop/cartare in,/shoppingis out. - Case is ignored. CDX servers lower-case the URL in their index key, so
example.com/Shop*andexample.com/shop*return the same rows, all in lower case. - A query string or fragment in the target is dropped.
--no-strictturns off the host check but keeps the path scope.
Note: older urx versions dropped the path, so
urx https://example.com/shopscanned all ofexample.com. Pass only the host for that. A run whose target has a path says so on stderr.
Host Validation
With --strict (the default) a URL is kept only if its host is one of the
targets. www.<target> counts as the target, so a site served only on www.
is kept. With --subs, any subdomain of a target is kept too. --no-strict
turns off the host check but keeps a target's path scope. --strict takes no
value; use --no-strict to turn it off (--strict false reads false as a
domain).
This applies to targets from positional arguments, --domain-list and stdin. A
target path narrows CDX queries and also filters every provider's results
client-side.
When validation removes more than half of the URLs left after the other filters
and --subs is off, urx prints a hint on stderr, even without -v:
[urx] strict host validation removed 812/1400 URLs; pass --subs to keep subdomains or --no-strict to keep all hosts
When the target has a path, the hint suggests dropping the path instead of
--no-strict. Under --no-strict it reads [urx] the target's path scope removed …. The hint is printed for batch output only (not under --stream).
--silent hides it.
Regular-expression Filtering
--patterns and --exclude-patterns are substring tests (every metacharacter
is literal). --match-regex and --filter-regex use
regex syntax:
--patterns |
--match-regex |
|
|---|---|---|
| Matching | substring | full regex syntax |
| Case | insensitive (both sides lower-cased) | sensitive; prefix (?i) to opt out |
| Multiple values | comma-separated (the flag may also be repeated) | repeat the flag; commas are never split |
The regex runs against the whole URL as collected (scheme, host, path, query),
so ^https:// and \.js$ both work. Several --match-regex values are ORed.
One --filter-regex hit drops a URL, and exclusion beats inclusion. A malformed
expression aborts the run at startup, before any archive is queried.
# versioned API paths only
urx example.com --match-regex '/api/v[0-9]+/'
# two alternatives, one per flag (a comma inside a regex stays intact)
urx example.com --match-regex '\.php$' --match-regex '/admin/[a-z]{3,8}$'
# drop build output
urx example.com --filter-regex '/(assets|static|dist)/'
Scope Files
--scope-file reads a bug bounty scope list as platforms write it, so you don't
have to turn it into anchored regexes (where a wrong anchor quietly widens the
scope).
# scope.txt: in scope
*.example.com
api.example.org
# out of scope, even though the wildcard above covers them
!admin.example.com
!*.internal.example.com
urx example.com --subs --scope-file scope.txt
# several programs at once; the files are unioned
urx --domain-list targets.txt --subs --scope-file scope-a.txt --scope-file scope-b.txt
Rules:
- One host pattern per line. A
!prefix excludes it. *.example.commatchesexample.comand every host under it (the bug-bounty reading). To exclude the apex, add!example.com.- A bare
example.commatches only that host: notwww., not subdomains. - A lone
*matches every host, for a file that is only a deny-list. #starts a comment that runs to the end of the line. Blank lines are skipped.- Exclusion always wins, as
--filter-regexbeats--match-regex. - A file with no include lines is a deny-list: everything is in scope except what it excludes.
- IPv6 literals must be bracketed (
[2001:db8::1]). They are exact hosts and can't be wildcarded. - Anything else (a port, a path, a wildcard in the middle) is a startup error naming the file and line.
--scope-file and --strict are separate checks and a URL must pass both. A
*.example.com line while querying the bare apex still needs --subs, because
strict mode drops the subdomains first. The scope applies to every provider, the
batch result, the --stream output and links found by --extract-links.
Archive-side Filtering
--from, --to, --archive-status, --archive-exclude-status,
--archive-mime and --archive-exclude-mime are sent to the archive's CDX
index, so filtered-out rows never come over the network.
# captures from 2018 to 2020 that the archive saw as 200
urx example.com --from 2018 --to 2020 --archive-status 200
# skip redirects, 404s and HTML
urx example.com --archive-exclude-status 301,302,404 --archive-exclude-mime text/html
# JSON endpoints, even ones with no file extension
urx example.com --providers wayback --archive-mime application/json
- They apply only to CDX-backed providers:
wayback,cc,arquivoand any--cdx-endpoint. Other providers ignore them, and urx warns when no CDX provider is enabled. --from/--totakeYYYY,YYYYMM,YYYYMMDDorYYYYMMDDhhmmss. Partial dates pad to the start (--from) or end (--to) of the period. The old names--wayback-from/--wayback-tostill work.- The CDX dialects differ:
wayback, --cdx-dialect classic endpoints |
cc, arquivo, pywb endpoints |
|
|---|---|---|
| Value matching | regex (--archive-status "30." matches any 3xx) |
substring (give the full value: 20 also matches 201) |
Positive list (--archive-status 200,301, --archive-mime a,b) |
works | skipped with a warning (repeated filters are ANDed, so it could never match) |
Exclusion list (--archive-exclude-*) |
works | works |
A --cdx-endpoint with no --cdx-dialect is treated as pywb for that warning.
--archive-status is the status the archive recorded at crawl time and costs
no extra requests. --check-status/--include-status give the target's status
now and re-request every URL.
Archive Metadata Filters
The archive-side filters above are free, but they only reach CDX providers and a
positive list is dropped on pywb servers. The eight --meta-* filters run
after collection on the merged capture metadata
of each URL, so lists work for every provider:
| Flag | Keeps |
|---|---|
--meta-first-seen-after <DATE> |
URLs whose oldest capture is on or after DATE |
--meta-first-seen-before <DATE> |
URLs whose oldest capture is on or before DATE |
--meta-last-seen-after <DATE> |
URLs whose newest capture is on or after DATE ("still alive as of") |
--meta-last-seen-before <DATE> |
URLs whose newest capture is on or before DATE ("dead since") |
--meta-mime <TYPE> |
URLs with one of these archived MIME types (image/* matches any subtype) |
--meta-exclude-mime <TYPE> |
everything except those |
--meta-status <CODE> |
URLs with one of these archived status codes (20x / 5xx patterns, as in --include-status) |
--meta-exclude-status <CODE> |
everything except those |
Dates take the same formats as --from/--to and pad the same way: an after
bound pads to the start of the period, a before bound to the end. So
--meta-first-seen-after 2020 --meta-first-seen-before 2020 means "first
archived during 2020".
# alive recently, HTML and images out of the way
urx example.com --providers wayback --meta-last-seen-after 2024 --meta-exclude-mime 'text/html,image/*'
# pages that died: nothing captured since 2019
urx example.com --providers wayback --meta-last-seen-before 2019
# JSON the archive served successfully
urx example.com --providers wayback --meta-mime application/json --meta-status 200
Using both kinds together is normal: archive-side filters cut what comes over
the network, --meta-* filters apply to the merged result.
Many URLs carry no metadata: those from non-CDX providers (otx, vt,
urlscan, zoomeye, github, bevigil, robots, sitemap), --files input
and cache hits. What happens to them depends on the filter:
- A positive filter (
--meta-mime,--meta-status, any date bound) drops them, since an absent value can't match. - An exclusion (
--meta-exclude-mime,--meta-exclude-status) keeps them. It drops only positive matches, like--filter-regexand--exclude-status.
--verbose reports the split ("… failed a predicate, … carried no archive
metadata to test"). When missing metadata removes the whole result set, urx says
so even without -v, since a cache hit would otherwise look like a target with
nothing to find. Pass --no-cache and a CDX provider to get metadata back.
The --meta-* flags are rejected under --stream, as --show-meta is: a
streamed URL is printed before the captures that complete its metadata arrive.
Collapsing Near-duplicates
--dedup-similar prints one line for a group of URLs that are the same endpoint
with different data (/post/1 … /post/99999).
A path segment counts as data when the whole segment is one of:
- a run of digits:
/post/1,/page/42 - a UUID:
/u/550e8400-e29b-41d4-a716-446655440000 - a 32/40/64-character hex digest (md5, sha1, sha256)
- a separated date:
/blog/2024-01-02/ - a long mixed-case token containing digits (session ids, signed blobs)
A segment that only contains digits stays, so /api/v1/ and /api/v2/ remain
distinct. Lower-case slugs stay too. Query strings are grouped by parameter
names only: ?q=cats&page=1 and ?q=dogs&page=7 collapse together, ?q=cats
alone does not.
Each group keeps its lexicographically smallest URL, so repeated runs over the
same data give the same output. --verbose reports how many URLs were
collapsed.
--normalize-url, --merge-endpoint and --dedup-similar can be combined and
run in that order. --merge-endpoint and --dedup-similar need the complete
result set and are rejected under --stream; --normalize-url works per URL
and can stream.
urx example.com --dedup-similar --verbose
urx --files urls.txt --normalize-url --merge-endpoint --dedup-similar
Parameter and Fuzz Views
--show-only-param only cuts each URL down to its query. These three views
replace the URL list with parameter data instead, using the same grouping as
--dedup-similar.
--params prints every query parameter name in the result set, once each,
sorted.
$ urx example.com --params
page
q
ref
sort
utm_source
--params-by-endpoint prints one line per endpoint: the endpoint, a space, and
the comma-separated parameter names seen on it. Id-like path segments collapse
to {id} as in --dedup-similar, so /post/1?a=1 and /post/2?b=2 become one
endpoint taking a,b. The endpoint includes scheme and host, so
a.example.com/search and b.example.com/search stay separate.
$ urx example.com --params-by-endpoint
https://example.com/post/{id} ref,utm_source
https://example.com/search page,q,sort
--fuzz-placeholder VALUE sets every query parameter value to VALUE and keeps
one URL per parameter signature. URLs without parameters are dropped. Each kept
URL has its real path (a {id} would not route) and is the smallest URL of its
group, so output is reproducible.
$ urx example.com --fuzz-placeholder FUZZ
https://example.com/post/1?ref=FUZZ
https://example.com/post/2?utm_source=FUZZ
https://example.com/search?q=FUZZ&page=FUZZ
https://example.com/search?q=FUZZ&sort=FUZZ
# into ffuf
urx example.com --fuzz-placeholder FUZZ | ffuf -w - -u FUZZ
# or dalfox
urx example.com --fuzz-placeholder FUZZ | dalfox pipe
Parameter names are taken from the raw query without decoding.
- All three need the complete result set and are rejected under
--stream. - They are mutually exclusive with each other and with the
--show-only-*views. The three--show-only-*views are also mutually exclusive with each other. - A command-line view replaces any configured
show_only_*view. A config file that sets more than one of those keys totrueis rejected. --params,--params-by-endpointand the--show-only-*views are rejected with any tester that requests URLs (--check-statusand the status filters,--check-title,--extract-links,--extract-js-endpoints,--archive-body,--expand-specs), because they rewrite URLs before those would be requested.--fuzz-placeholderoutput is real URLs and can be checked.
Wordlist Output
-f wordlist prints every path segment and query parameter name the run saw,
deduplicated across the run and sorted, one per line.
$ urx example.com -f wordlist
admin
api
page
post
q
ref
search
sort
users
users.json
utm_source
v1
- Segments that look like data are dropped, using the
--dedup-similartest (numbers, UUIDs, dates, session tokens). A segment whose stem is an identifier goes too (1234.html); a name with a number attached (article-1234.html) stays. - Case is kept. Paths are case-sensitive on most servers, so lower-casing
WebResource.axdwould give a word that 404s, and/Adminnext to/adminis worth keeping. - Batch only:
--streamrejects it. - Per-URL fields (status,
--show-sources, capture metadata) are not emitted. --output-dirwrites wordlists as.txt.[output].formatin the config file acceptswordlist.
# build a target wordlist and fuzz with it
urx example.com --subs -f wordlist -o words.txt
ffuf -w words.txt -u https://example.com/FUZZ
Streaming Output
By default urx collects everything, then filters, sorts and prints once, so
nothing appears until the slowest provider finishes. --stream writes each URL
as soon as its provider reports it.
# matches appear right away
urx big-target.com --stream | grep admin
# jsonl stays valid while it is being written
urx big-target.com --stream -f jsonl | jq -r 'select(.url | test("/api/")) | .url'
- Streamed URLs pass the same filters and host validation as a batch run and are still deduplicated.
- Output is unsorted (provider-completion order). Pipe through
sortif you need order. - Formats:
plain,jsonl,csv.jsonis refused (usejsonl; a JSON array has to know its last entry), and so iswordlist(no term is known to be new until every URL is in). - The cache is bypassed.
--silentdoes not hide streamed URLs.- When the reader goes away (
| head -1), fetching stops and the run exits 0. - Memory use is lower: only the set of URLs already written is kept.
--normalize-url, the--show-only-*views,--scope-fileand--archived-discoverywork with it.
Options that need the complete result set are rejected at startup, with a
message naming each one: --merge-endpoint, --dedup-similar,
--check-status, --include-status, --exclude-status, --check-title,
--extract-links, --extract-js-endpoints, --archive-body,
--expand-specs, --incremental, --show-sources, --show-meta, the
--meta-* filters, --params, --params-by-endpoint, --fuzz-placeholder,
--output-dir and --files.
Authenticated and Custom Requests
--check-status, --extract-links, --extract-js-endpoints and
--expand-specs re-request collected URLs from the target. -H adds headers to
those requests:
urx example.com --check-status -H "Authorization: Bearer $TOKEN"
urx example.com --extract-links --cookie "session=abc; role=admin"
urx example.com --check-status --user-agent "acme-security-scan/1.0"
-His repeatable and takesName: value. A malformed one stops the run, so an anonymous scan can't pass for an authenticated one.--cookieand--user-agentare shorthands for those headers. A later value for the same name replaces an earlier one.- These headers never reach an archive. Only components that talk to the
target send them: the four testers above and the
robots/sitemapproviders. Other providers, and--archive-bodyreplays, query web.archive.org, index.commoncrawl.org or third-party APIs, which have no use for your session cookie. Archive queries keep urx's own User-Agent, which--random-agentstill rotates. - They follow
--network-scope. Under--network-scope providersthe testers get none (an authenticated--check-statusgoes out anonymous); under--network-scope testerstherobots/sitemapproviders get none.
In the config file:
[network]
header = ["X-Env: staging", "X-Team: appsec"]
cookie = "session=abc"
user_agent = "acme-security-scan/1.0"
Any -H on the command line replaces the configured header list, so a run can
drop the configured headers without editing the file.
Link Extraction
--extract-links re-fetches every URL left after filtering and pulls links out
of the HTML. It reads every URL-bearing tag:
| Tag | Attribute | Typically finds |
|---|---|---|
<a> |
href |
Navigation |
<script> |
src |
JavaScript bundles |
<link> |
href |
Stylesheets, icons, preloads, canonical/alternate URLs |
<form> |
action |
Endpoints that are never linked |
<iframe> |
src |
Embedded apps and widgets |
<img> |
src |
Images, including CDN hosts |
<source> |
src |
Media alternatives inside <video> / <audio> |
<object> |
data |
Legacy embedded objects |
<embed> |
src |
Legacy plugin content |
<meta http-equiv="refresh"> |
content |
Markup redirects (0; url=...) |
- Relative URLs resolve against the page, honouring
<base href>. javascript:,mailto:,tel:,data:,about:,blob:and bare#fragmentreferences are skipped.- Duplicates are collapsed.
- Found links go through the same filters, host validation and output
transforms as provider URLs. The filters also run on the collected URLs
before any page is fetched, so
--extract-links -e jsdrops the HTML pages first and finds nothing. Filter the output instead. - Found links are not status-checked.
--check-statuscovers the collected URLs only. - Only successful responses that look like markup are parsed. Each body is capped at 10 MiB.
# one hop deeper, keep only JavaScript
urx example.com --extract-links | grep -E '\.js(\?|$)'
# extraction uses the network settings too
urx example.com --extract-links --proxy http://localhost:8080 --timeout 20
JavaScript Endpoint Extraction
--extract-js-endpoints fetches collected JavaScript and pulls endpoints out
of its string literals: fetch("/api/v2/users"), axios.post("/graphql"),
`/api/orders/${id}`. -e js only collects bundles; this reads them.
URLs whose extension is clearly not script (images, fonts, CSS, archives,
.json, .map, ...) are skipped without a request. The rest are fetched and
sorted by Content-Type:
| Response | Scanned |
|---|---|
| JavaScript type | Whole body |
| HTML | Inline <script> blocks only |
.js/.mjs/.cjs/.jsx/.ts/.tsx URL served as text/plain, application/octet-stream, or with no Content-Type |
Whole body, as script |
Any other URL with no Content-Type |
Inline <script> blocks, as HTML |
| Any other type | Discarded unread |
Extracted:
- Quoted absolute and origin-relative paths:
"/api/v2/users",'/graphql'. - Full and protocol-relative URLs:
"https://api.example.com/v1","//cdn.example.com/x". - The URL argument of
fetch(...),axios.get/post/...(...),axios({url: ...}),$.ajax(...)andXMLHttpRequest.open(method, ...), including thebase + "/path"form. - The static prefix of a template literal:
`/api/users/${id}`→/api/users/. - ES-module chunk imports and asset references:
import("./chunk-ab12.js").
Relative paths:
/api/xresolves against the script's origin, as the browser does../chunk.jsresolves against the script's own URL, as an ES-module import does.- Bare relative paths (
api/v1/x) and./xrequest arguments depend on the page that loaded the bundle, which urx doesn't know. They resolve against the origin root.
Minified bundles produce a lot of noise, so the extractor errs toward dropping.
The code comments in src/testers/js_endpoint_extractor.rs show the real-bundle
case behind each rule.
| Dropped | Why |
|---|---|
MIME types (image/png, application/json, */*) |
Header values shaped like a two-segment path; the most common false positive |
"/", "/x", "//", "./", "../" |
Path-join fragments and comment delimiters (fetch("/") is still kept) |
Sourcemap directives, //#, //@ |
Not URLs |
Base64 and data: payloads |
Inline images and fonts; hex content hashes and hashed filenames survive |
CSS shorthand and ratios (12px/1.5, 16/9) |
Style values |
Date formats (MM/DD/YYYY, HH/mm) |
Formatting tokens |
Bare extensions (.js, /.png) |
File-type checks |
Regex sources and tag fragments (/^\/api/(\d+), </div>) |
Excluded by the character class, so they never match |
Strings closed by a different quote ('/g,"), trailing , ; : |
Artifacts of regex literals next to strings |
Bare relative paths with no leading / or . (react/jsx-runtime, en/US) |
Package paths and locale tags. Kept only when a segment has three or more characters and there are three or more segments, a query string, or a dot in the last segment (app/main.js). fetch / axios / XHR arguments are exempt |
Package and source-tree paths (@scope/pkg/..., node_modules/..., ./src/..., lib/esm/...) |
Import specifiers and webpack module keys |
./x without a fetchable extension (require("./utils"), {"./zlib/deflate":46}) |
CommonJS module specifiers (one real bundle had ~50 from jszip and pako alone) |
Path suffixes after + ("/users/" + id + "/avatar" → /avatar) |
Only the prefix is a route; base + "/api/x" is still kept |
XML namespaces and schema hosts (www.w3.org, schema.org) |
Boilerplate in every SVG-bearing bundle |
A string passed to a request call is known to be a URL and skips the length and bare-relative rules.
- Each body is capped at 10 MiB. Fetches use
--timeout,--retries,--proxy,--insecure,--random-agentand--rate-limit. --max-js-filescaps files fetched per run (default 500,0for unlimited).- Found endpoints go through the same filters and host validation as
everything else. Strict mode (the default) drops off-site URLs;
--no-strictkeeps them. - With
-e js, only.jsendpoints survive the filters. The extractor already picks JavaScript itself, so leave-e jsoff to get API paths. - Found endpoints are not status-checked.
--check-statuscovers the collected URLs only. - Rejected under
--stream.
# mine every collected script for API paths
urx example.com --extract-js-endpoints
# keep API-looking paths (filter the output; --patterns would drop the
# bundles before they are read)
urx example.com --extract-js-endpoints | grep -E 'api|graphql'
# at most 50 bundles, one request per second
urx example.com --extract-js-endpoints --max-js-files 50 --rate-limit 1
API Specification Expansion
--expand-specs fetches collected OpenAPI, Swagger and GraphQL documents and
adds every route they describe to the results. The other testers skip these
files: --extract-links parses HTML, --extract-js-endpoints drops
application/json bodies, and --archive-body runs the HTML parser.
# find the specs and expand them in one run (only-api also filters the
# expanded routes; drop it to keep every documented path)
urx example.com --preset only-api --expand-specs
# bound and pace it
urx example.com --expand-specs --max-spec-files 10 --rate-limit 2
# an API that no longer exists: read the archived document
urx example.com --archive-body --expand-specs
What is expanded (JSON or YAML):
- OpenAPI 3.x:
servers[].url(absolute, relative to the document, or templated, with{var}taken fromvariables[var].default, else the firstenumvalue) crossed with everypathskey. A path item's ownserversoverride the document's. - Swagger 2.0:
schemes×host+basePath, each part falling back to the matching part of the document's own URL when omitted.ws/wssschemes are dropped. - GraphQL introspection (
{"data":{"__schema":…}}or unwrapped): one URL per query, mutation and subscription field, written as the endpoint plus?query=…. A schema saved as a file resolves to its endpoint (/graphql/schema.json→/graphql).
Which URLs are fetched: the path must contain a spec marker (swagger,
openapi, api-docs, apidocs, api_docs, graphql, introspection), and
if the URL has an extension it must be json, yaml or yml. So
swagger-ui.html and swagger-ui-bundle.js cost no request. Then
Content-Type decides: a clearly unrelated type (HTML, image, JavaScript) skips
the body, while vague types (text/plain, application/octet-stream) fall back
to the extension and then the first byte of the body, so an untyped
/v3/api-docs still parses.
Details
- Path templates are kept as written (
/users/{id}, not/users/%7Bid%7D).--normalize-urlencodes them if you ask for it. --max-spec-files(default 50) caps documents fetched per run;0means unlimited.- Each body is capped at 10 MiB. A YAML document whose aliases would copy more than 10 MiB of nodes is refused before parsing ("billion laughs" guard).
- Only the first document of a multi-document YAML stream is read.
- Found URLs go through the same filters, host validation and output transforms as provider URLs.
- Rejected under
--stream. - With
--archive-bodyon, an archived spec is read as a spec instead of being handed to the HTML link extractor. No extra requests: the body was already fetched and counted against--archive-body-limit. This recovers routes after the live API was retired, moved behind auth, or its document removed.
Archived Response Bodies
--archive-body fetches the bodies the Wayback Machine stored for the
collected URLs and runs the --extract-links extraction on them. This finds
links on pages that no longer exist.
Each collected URL with a capture timestamp is replayed raw:
https://web.archive.org/web/<timestamp>id_/<url>
id_ turns off the Wayback toolbar and link rewriting, so the response is the
original bytes with the original Content-Type. Relative links resolve against
the captured URL.
# links from the archived bodies of everything the CDX providers found
urx example.com --archive-body
# bound and pace it; the archive is one host however many URLs there are
urx example.com --archive-body --archive-body-limit 200 --rate-limit 5
# only the JavaScript those pages referenced back then
urx example.com --archive-body | grep -E '\.js(\?|$)'
Why this needs far fewer requests than waymore
Captures with the same CDX digest have identical bodies, and archives are full
of them: ?utm_source= variants, /index.html next to /, tracking-parameter
permutations. Tens of thousands of URLs often collapse to a few thousand
distinct bodies. urx fetches each digest once and skips later URLs with the same
digest. waymore downloads one response per URL and caps the volume with
-l 5000.
--archive-body-limit (default 500) counts distinct bodies, so duplicates
don't count against it. --verbose reports how many URLs were skipped as
duplicates, fell past the limit, or had no capture to replay.
Mining archived JavaScript
Bundles are named by build hash, so app.a3f9c2.js 404s after a redeploy and
live --extract-js-endpoints can't read it. With both flags, urx mines the
archived bundles and the inline <script> blocks of archived pages:
urx example.com --archive-body --extract-js-endpoints
An archived body is checked for script before markup, because a capture with
no Content-Type would otherwise go to the HTML parser and yield nothing.
Keeping the bodies
--archive-body-dir saves the replayed bodies at no extra request cost, for
grepping comments, inlined tokens or stack traces:
urx example.com --archive-body --archive-body-dir ./corpus
grep -ri "api[_-]key" ./corpus
- Files are named from the URL plus a hash of it (the slug is lossy).
corpus/index.jsonlmaps each file to its URL, capture timestamp, digest and content type. - Only text-like bodies are stored (HTML, script, JSON, XML, CSS, plain text).
- An unwritable directory stops the run at startup. A failure on one file is reported and the run keeps its links.
--archive-body-dirwithout--archive-body(includingarchive_body_dirset only in the config file) is rejected at startup.
Details
- Only URLs with a capture timestamp qualify. The CDX providers (
wayback,cc,arquivo, and any--cdx-endpoint) supply one;--filesinput, non-CDX providers and cache hits (the cache stores URLs only) have none. urx says so when there is nothing to replay. Pass--no-cachefor fresh captures. - The newest capture of each URL is replayed, and its digest is the dedup key. A timestamp from another archive lands on the nearest Wayback capture. A URL the Wayback Machine never saw returns 404 and is skipped quietly.
- Captures the archive recorded as errors are not mined, as
--extract-linksignores live error pages. Non-markup bodies are skipped unparsed. - Found links go through the same filters, host validation and output transforms as provider URLs.
- Each body is capped at 10 MiB.
--rate-limit,--rate-limit-by wayback=N,--parallel,--proxy,--timeoutand--retriesapply to replays. Under--network-scope providers, replays count as testers and get none of these settings.- Rejected under
--stream.
Archived robots.txt and sitemap.xml
The robots and sitemap providers read the live files. --archived-discovery
also reads every distinct version the Wayback Machine stored. Old Disallow:
lines and old sitemaps name paths the live files no longer mention.
# every archived version of robots.txt and sitemap.xml, plus the live ones
urx example.com --archived-discovery
# bound and pace it; both archived providers follow --rate-limit-by
urx example.com --archived-discovery --archived-discovery-limit 20 --rate-limit-by robots=2,sitemap=2
# only versions captured in a given period
urx example.com --archived-discovery --from 2014 --to 2016
# only the robots.txt history
urx example.com --archived-discovery --exclude-sitemap --show-sources
How it works
-
Versions are listed with one CDX query per file name (
robots.txt,sitemap.xml,sitemap_index.xml,sitemap.txt):/cdx/search/cdx?url=<domain>/robots.txt&fl=original,timestamp,statuscode,digest &collapse=digest&filter=statuscode:2..collapse=digestfolds consecutive identical captures into one row, so you get one row per change. The status filter keeps it cheap: the CDX urlkey mergeswww.and the apex, and their interleaved301/200rows otherwise break the collapse. On github.com/robots.txt that is 325,036 rows without the filter and 13,909 with it, for the same 107 versions. Remaining duplicate digests are dropped client-side. -
Each version is replayed raw (
/web/<timestamp>id_/<original url>) and read by the same parser as the live file, with the same absolute-path and pattern-skipping guards. Its paths land on the host that served it. An archived<sitemapindex>is followed into its children at the same timestamp, with the same same-host rule as the live walk. -
Captures recorded as anything but success (github.com's robots.txt was a 401 for part of 2007) are never requested. They, and versions the replay endpoint refuses, are counted under
--verboseonly.
Details
--archived-discovery-limit(default 50) caps documents fetched per domain by each archived provider. Newest versions are read first, and nested sitemaps count.--verbosesays when the cap cut the list short.- The archived reads show as "Robots.txt (archived)" and "Sitemap (archived)"
in
--statsand--show-sources, but run under therobotsandsitemapids. So--exclude-robots,--exclude-sitemapand--rate-limit-by robots=N/sitemap=Ncover live and archived reads together. --from/--tonarrow which versions are read. The other--archive-*filters don't apply to a version history.- Bodies are capped like the live files: 1 MiB for robots.txt, 50 MiB per sitemap document.
- It is a provider, so it works with
--streamand is cached like any other provider (the cache key includes the flag).