URLInsane

August 3, 2026 · View on GitHub

Go Report Card Go Doc PkgGoDev Release Build Status

Urlinsane is a tool for detecting domain typosquatting and supporting OSINT investigations, designed to operate on multilingual target domains. It helps identify threats such as typosquatting, brandjacking, URL hijacking, phishing, fraud, corporate espionage, supply chain attacks, and more. This command-line tool generates and scans for potential typosquatting variants of a domain, assisting in uncovering and mitigating security risks.

It's inspired by URLCrazy, Dnstwist, and a few other libraries and tools I was researching at the time.

Full documentation: rangertaha.github.io/urlinsane — the CLI reference, the engine design, and the keyboard model, as a book. The same pages live in this repo as docs/CLI.md, docs/DESIGN.md and docs/KB.md.

Installation

This tool is primarily intended for Linux operating systems.

Linux

Download the binary, remove the previous version, and install it in /usr/local/bin:

wget https://github.com/rangertaha/urlinsane/releases/download/0.8.2/urlinsane-0.8.2-linux-amd64 
chmod +x urlinsane-0.8.2-linux-amd64 
sudo mv urlinsane-0.8.2-linux-amd64  /usr/local/bin/urlinsane

Usage

Generate variations of a target and gather information on them with the typo command:

urlinsane typo example.com

Typosquatting is not limited to domains, and the target's kind is detected from the string alone — there is no --type flag:

urlinsane typo acme.com                 # domain
urlinsane typo bob@acme.com             # email: varies bob, acme.com, and the address
urlinsane typo npm:lodash               # package on a named registry
urlinsane typo github.com/acme/tool     # repo
urlinsane typo bobsmith                 # username

An optional first positional narrows what gets varied, without changing how the target is read:

urlinsane typo username acme.com/bob    # vary only bob
urlinsane typo domain bob@acme.com      # vary only acme.com

typo options

urlinsane typo [<scope>] <target> [flags]

FlagAliasDefaultDescription
--depth-d3Observation hops from the seed
--algorithm-aallRestrict variant generation to these algorithm IDs; ^id excludes
--filter-fSelect report rows: live, absent, unknown, untried, risk>SEV, type=NAME, depth<=N
--output-otabletable, json, ndjson, csv, dot
--saveWrite the report to a path; format from the extension
--save-graphPersist the graph to the store and print its root CID; urlinsane report <target> renders it again
--fail-onExit 2 if any finding reaches a severity — the CI gate
--verbose-vInclude provenance and engine belief
--explainCompile and print the plan without running it
--listtypes, relations, operators, algorithms, languages, keyboards, formats, filters

--filter selects rows in the report, never work in the scan — narrowing the scan is what --depth, --algorithm and the scope positional do.

Exit codes: 0 clean, 1 execution error, 2 a finding at or above --fail-on.

docs/CLI.md is the full reference, including the flags that are specified but not yet built. docs/DESIGN.md §12 is the reasoning behind the interface. Both are also in the book.

List what a build has registered:

urlinsane typo --list algorithms
urlinsane typo --list keyboards

What a build registers

KindCount
Node types10asn, domain, email, ip, package, platform, registrant, repo, tld, username
Algorithms32generate variants of a name
Operators4227 variant operators, one per algorithm, plus 15 that decompose and observe
Keyboards133distinct key-adjacency sets, from the 203 layouts pkg/kb ships
Languages113codes in dataset.db; 30 have curated trees under datasets/languages/
Formats5table, json, ndjson, csv, dot

Counts are what --list prints on a build of this tree with internal/config/dataset.db imported. Types, algorithms, operators and formats come from Go registries, so they are fixed by the binary; keyboards come from pkg/kb, also compiled in. Languages come from the dataset database in ~/.config/urlinsane/, which is extracted only when absent — an older copy left there from a previous version will list something else. geo and the pkg/usr/repo operators are conditional (see below), so 42 is the count when their data is present.

Languages and keyboards are data, not plugins: a language is a directory under datasets/languages/, a keyboard a layout in pkg/kb, and neither needs Go code. Output formats are a closed set the report projects into. What remains extensible is internal/plugins — operators, analyzers and algorithms — one directory per plugin, grouped by kind (decompose, variant, observe, analyze, report).

Language IDs

urlinsane typo --list languages

Languages are two-letter directory names under datasets/languages/; Pashto is ps and Latin is la. Note that this lists every code the dataset database knows — 113 — not the ones with data behind them: 31 carry vocabulary, and 30 have a curated tree in this repo.

Language Datasets (datasets/languages/)

The repo ships with a datasets/languages/<lang>/ structure (e.g. numeral.lst, homoglyph.lst, homophone.lst, positive.lst, negative.lst, etc).

These files are the authored source, hand-curated per language, and nothing generates them. Load them into the dataset database with:

go run ./cmd/datasets import datasets

A sync-languages command used to generate this tree from the language plugins. That pointed the wrong way — the curated lists were the artefact and the plugins were built from them — and it was removed along with the language plugins themselves. Languages are data now, not code: adding one means adding a directory here and re-importing.

Keyboard Layouts

Keyboard layouts are data, compiled in from pkg/kb: 203 shipped layouts, which collapse to the 133 distinct key-adjacency sets the algorithms actually run over. To list them:

urlinsane typo --list keyboards

Algorithms

Algorithms generate plausible variants of a name. --list algorithms prints this table for the build you have.

Applies to is blank where an algorithm binds by capability rather than by type — those run on any nameable node, domain or package or handle alike.

IDNameApplies toDescription
aciAdjacent Character InsertionanyInsert a character adjacent on the keyboard.
acsAdjacent Character SubstitutionanyReplace a character with a keyboard neighbour.
afxAffix Squattingpackage, repo, usernameAdd a plausible prefix or suffix.
bfBit FlippinganyFlip one bit of a character — bitsquatting.
cbCombo SquattinganyAppend or prepend a common keyword.
cmCommon MisspellingsanyApply a curated misspelling for the language.
cnsCardinal SubstitutionanySwap a number for its cardinal word, and back.
coCharacter OmissionanyDrop a character.
crCharacter RepetitionanyDouble a character.
csCharacter SwappinganyTranspose two adjacent characters.
dhsDot Hyphen SubstitutionanySwap dots and hyphens.
diDot InsertionanyInsert a period.
doDot OmissionanyRemove a period.
fsdDelegated SubdomaindomainPut the name under a host that gives subdomains away: paypal.duckdns.org.
giGrapheme InsertionanyInsert a grapheme from the language's set.
grGrapheme ReplacementanyReplace a grapheme with another.
hiHyphen InsertionanyInsert a hyphen.
hoHyphen OmissionanyRemove a hyphen.
hrHomoglyph ReplacementanyReplace a character with one that looks the same.
hsHomophone SubstitutionanyReplace a word with one that sounds the same.
nscNamespace Confusionpackage, repoMove a name between namespaces or scopes.
onsOrdinal SubstitutionanySwap a number for its ordinal word, and back.
rarRepetition Adjacent ReplacementanyDouble a character, then replace the double with a neighbour.
sepSeparator Substitutionpackage, repo, usernameSwap the separator a registry allows.
siSubdomain InsertiondomainInsert a subdomain label.
sldWrong Second-Level DomaindomainSwap the second level under a ccTLD: bbc.co.ukbbc.org.uk.
spSingular PluraliseanyMake a word singular or plural.
tldWrong TLDdomainSubstitute a different public suffix.
tosToken Order SwapanyReorder the words: shop-onlineonline-shop.
tliTLD InsertiondomainAppend a suffix so the whole name becomes a subdomain: example.com.br.
vsVowel SwappinganySwap one vowel for another.
xhsCross-language HomophoneanySwap for a spelling that sounds the same in another language: youtubeyutup.

Operators

An operator is what expands or observes the graph. Where the old collectors ran in a fixed order over a list of domains, an operator declares what data pattern it binds to and what it emits, and the scheduler decides what runs when. --list operators prints the plan-eligible set:

IDBinds onEmits
decompose.domaindomainTLD_OF
decompose.emailemailLOCAL_PART, DOMAIN_OF
decompose.packagepackageOWNER
decompose.reporepoHOSTED_ON, OWNER
dns-adomainRESOLVES_TO
dns-mxdomainMX
dns-nsdomainNS
dns-cname dns-txtdomainprops only
ptripPTR_TO
whoisdomainREGISTERED_BY
idndomainprops only
geoipprops only — needs the geolocation database
pkg usr repopackage, username, repoEXISTS_ON — needs the source lists

Plus one operator per algorithm, all emitting VARIANT_OF.

Binding is by data, not by producer. ptr binds to any ip, so it runs on addresses whether dns-a found them or something else did — which is what lets a new operator slot in without anyone rewiring an order.

Three-state existence. An operator that cannot reach a registry reports unknown, never absent. "We asked, it is not there" and "we could not tell" are opposite conclusions, and collapsing them turns a broken network into a clean bill of health.

geo, pkg, usr and repo are omitted from the plan when the data they need is missing, rather than failing at runtime — so --list operators shows them only on a build that has it. pkg, usr and repo need the source lists, which dataset.db now carries, and appear; geo needs the MaxMind database extracted into ~/.config/urlinsane/, and drops out when that is absent or unreadable.

Output formats

FormatDescription
tablePretty table with colour styling; the default
jsonOne document, written when the scan ends
ndjsonOne object per node
csvComma-separated values
dotGraphviz — the graph, not a flattened list
urlinsane typo acme.com -o json | jq '.nodes[] | select(.exists=="live")'
urlinsane typo acme.com --save report.csv      # format from the extension

--save also accepts .txt/.text for table and .gv for dot. Anything else is an error rather than a guess, and a saved file is never coloured.

--filter selects rows, not columns, and it applies to the report rather than the scan — so re-filtering never costs another lookup.

Status

The engine is mid-rewrite, from a linear plugin pipeline to a graph engine. docs/DESIGN.md is the design; docs/CLI.md §9 tracks what is specified but not yet wired up.

Done, and worth saying how it differs from the plan:

  • The DAG replaced the pipeline. The original idea was Terraform-style declared dependencies between plugins. That is not what shipped: an operator declares what data pattern it binds to, and the scheduler matches. Declared dependencies made plugin order load-bearing and the cache unsound; binding by data means a new operator needs no rewiring.
  • Reference data moved into SQLite (dataset.db): vocabulary and weighted transitions, replacing a large body of generated Go. Results did not — they are an IPLD content-addressed graph, so two identical scans address identically and "what changed since last week" is a CID comparison.
  • Languages and keyboards stopped being plugins. 30 curated dataset directories and 203 keyboard layouts built from kbdlayout.info, neither needing Go code.
  • Cross-scan diffing exists in internal/store.
  • Saving and replaying scans. typo --save-graph writes the graph to the store; urlinsane report <target> renders it again from the stored blocks, and report --scans <target> lists what has been saved. Plugin settings in ~/.config/urlinsane/config.yaml reach the plugins that declare them.

Open:

  • Flags that are specified but not built — --quiet, --why, --ledger, --tui, and --ttl/--resume cross-run caching (docs/CLI.md §9).
  • An advanced keyboard model with layer-shifting.
  • DNS queries against several resolvers.
  • Dataset updates downloadable rather than embedded, to cut binary size.
  • LLM assistance for generating language datasets, and as a judgement operator over variants.
  • Reporting confirmed squats back to a shared corpus, so the transition weights can be learned from observed cases instead of being uniform placeholders.

Other Tools

NameLanguageDescription
UrlcrazyRubyURLCrazy is an OSINT tool to generate and test domain typos or variations to detect or perform typo squatting, URL hijacking, phishing, and corporate espionage.
DNSTwistPythonDomain name permutation engine for detecting homograph phishing attacks, typo squatting, and brand impersonation
DomainFuzzJavaScriptDomain name permutation engine for detecting typo squatting, phishing and corporate espionage

Authors

License

This project is licensed under the GPLv3 License - see the LICENSE file for details