Typosquatting detection: find lookalike domains in Google results, ads and certificate logs
To detect typosquatting that reaches customers, combine three sources: a list of permutation candidates of your domain (what could be registered), certificate transparency logs (what someone got a certificate for), and Google results and ads for your branded queries (what is being shown to people searching for you). The third source ranks the other two, because a lookalike that ranks for your brand name or runs ads on it is exposed right now. The workflow below is for brand owners defending their own names.
Typosquatting is registering a domain that differs slightly from a legitimate one to capture mistyped traffic or impersonate the brand. Combosquatting adds a word instead of changing letters (acme-login.com, acmeoutlet.shop). Both are called lookalike domains below.
Permutation candidates
Candidates are generated from your registered domain by a small set of transformations. For acme.com:
| Technique | Example |
|---|---|
| Omission | acm.com, ame.com |
| Repetition | accme.com, acmme.com |
| Transposition | amce.com, came.com |
| Adjacent-key replacement | acne.com, scme.com |
| Homoglyph | acrne.com (rn for m), internationalised domains with lookalike Unicode characters |
| Hyphenation | ac-me.com |
| Vowel swap | acma.com, ecme.com |
| TLD swap | acme.co, acme.shop, acme.net |
| Combosquatting | acme-login.com, acmesupport.com, myacme.com |
dnstwist
You do not need to write a generator from scratch. dnstwist is an open-source “domain name permutation engine for detecting homograph phishing attacks, typo squatting, and brand impersonation” (README, checked 2026-09-17). It generates variants and checks which are registered, can filter to registered domains only, detects rogue MX hosts, compares page similarity with fuzzy hashes and screenshot similarity with perceptual hashes, and exports CSV and JSON. It is Apache-2.0 licensed and available through pip, several Linux package managers, Homebrew and Docker, with a browser version at dnstwist.it.
A typical defensive workflow exports registered candidates daily to a file your pipeline reads.
A minimal generator for matching
For matching search results you also want a fast in-process check. The generator below produces the common single-edit variants of the label and a combosquatting test; use dnstwist’s output as the richer candidate list alongside it.
import string
KEYBOARD = {"a": "qwsz", "c": "xdfv", "e": "wsdr", "m": "njk", "o": "iklp", "i": "ujko", "n": "bhjm",
"s": "awedxz", "r": "edft", "t": "rfgy", "l": "kop", "u": "yhji"}
HOMOGLYPH = {"m": ["rn"], "w": ["vv"], "l": ["1", "i"], "o": ["0"], "i": ["1", "l"], "e": ["3"]}
COMBO_WORDS = ("login", "signin", "secure", "verify", "support", "help", "account", "outlet", "store",
"shop", "sale", "official", "app", "pay", "billing")
def variants(label: str) -> set[str]:
out = set()
for i in range(len(label)):
out.add(label[:i] + label[i + 1:]) # omission
out.add(label[:i] + label[i] + label[i:]) # repetition
if i < len(label) - 1:
out.add(label[:i] + label[i + 1] + label[i] + label[i + 2:]) # transposition
out.add(label[:i + 1] + "-" + label[i + 1:]) # hyphenation
for k in KEYBOARD.get(label[i], ""):
out.add(label[:i] + k + label[i + 1:]) # adjacent key
for g in HOMOGLYPH.get(label[i], []):
out.add(label[:i] + g + label[i + 1:]) # ASCII homoglyph
out.discard(label)
return {v for v in out if v and all(c in string.ascii_lowercase + string.digits + "-" for c in v)}
def classify(host: str, brand: str, owned: set[str], typo_labels: set[str]) -> str | None:
host = host.lower().removeprefix("www.")
if not host or any(host == d or host.endswith("." + d) for d in owned):
return None
labels = host.split(".")
registrable = labels[-2] if len(labels) >= 2 else labels[0]
if host.startswith("xn--") or any(l.startswith("xn--") for l in labels):
return "idn" # punycode: inspect manually
if registrable in typo_labels:
return "typo"
squashed = registrable.replace("-", "")
if brand in squashed and squashed != brand and any(w in squashed for w in COMBO_WORDS):
return "combo"
if brand in squashed or any(brand in l for l in labels[:-2]):
return "brand_in_host" # e.g. acme.example-shop.com
return None
registrable = labels[-2] is a simplification that is wrong for suffixes such as co.uk. In production, split hosts with a Public Suffix List library.
Certificate transparency
Certificate transparency logs are “append-only” and “publicly auditable”, and a CT monitor subscribed to your domain sends updates when certificates for it are logged (how CT works, checked 2026-09-17). Because Chrome and Safari help enforce CT, a lookalike site using a publicly trusted HTTPS certificate usually leaves a record.
crt.sh, operated by Sectigo, searches those logs by identity (domain or organisation name), certificate fingerprint or crt.sh ID (crt.sh). Useful defensive checks:
- New certificates for exact permutation candidates from your list.
- Certificates whose names contain your brand label outside your own domains.
- Certificates for subdomains that imitate you on unrelated domains (
acme.login-portal.example).
A certificate is evidence that a name was prepared for HTTPS, not that it is malicious. Many lookalikes are defensive registrations, parked domains or unrelated businesses.
What search results and ads show
Permutation lists and certificates tell you what exists. Google tells you what is being shown to people who search for you. Run branded queries through this API and read three fields:
| Field | Meaning for typosquatting |
|---|---|
result.organicResults[].link |
A lookalike ranking organically for your brand, product or support queries |
result.ads[].domain |
The advertiser domain of a text ad (type RESULT) on your branded query |
result.ads[].displayedUrl |
The URL shown in the ad; compare its host with domain and with your owned domains |
Also check result.aioverview.sources[].url when you request include.aioverview, since a lookalike cited in an AI Overview is exposed in a different way.
Queries that attract impersonators
Impersonators target moments when a user is ready to type credentials or pay:
acme login,acme sign in,acme accountacme customer service,acme support phone number,acme refundacme app downloadacme outlet,acme sale,acme discount code- Each product name plus
official
Run each on desktop and a mobile device (ios or android) in each market you sell in, because ads and result layouts differ.
Submit the checks
import datetime as dt, os, requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
HOOK = {"url": "https://hooks.example.com/lookalikes"}
QUERIES = ["acme login", "acme customer service", "acme app download", "acme outlet", "acme discount code"]
def submit(countries=("US", "GB")):
now = dt.datetime.now(dt.timezone.utc)
slot = f"{now:%Y-%m-%d}T{now.hour - now.hour % 6:02d}"
tasks = [{"taskType": "GOOGLE",
"payload": {"query": q, "country": c, "device": device},
"idempotencyKey": f"typo|{q}|{c}|{device}|{slot}",
"priority": 8, "webhook": HOOK}
for q in QUERIES for c in countries for device in ("desktop", "ios")]
res = requests.post(f"{API}/v1/async/task/batch", json=tasks, headers=HEADERS, timeout=60)
res.raise_for_status()
return res.json()["summary"]
Extract and match
from urllib.parse import urlsplit
BRAND = "acme"
OWNED = {"acme.com", "acme.co.uk", "acme-status.com"}
TYPO_LABELS = variants(BRAND) | set(load_dnstwist_labels()) # your daily dnstwist export
def hostname(value):
if not value:
return ""
if "://" not in value:
value = "https://" + value
return (urlsplit(value).hostname or "").lower()
def sightings(result):
for o in result.get("organicResults", []):
yield "organic", o.get("position"), hostname(o.get("link")), o.get("link"), o.get("title")
for a in result.get("ads", []):
if a.get("type") != "RESULT":
continue
d, shown = hostname(a.get("domain")), hostname(a.get("displayedUrl"))
yield "ad", a.get("position"), d, a.get("url"), a.get("title")
if shown and shown.removeprefix("www.") != d.removeprefix("www."):
yield "ad_displayed", a.get("position"), shown, a.get("displayedUrl"), a.get("title")
for s in (result.get("aioverview") or {}).get("sources", []):
yield "aio_source", s.get("position"), hostname(s.get("url")), s.get("url"), s.get("label")
def candidates(result):
for channel, position, host, url, title in sightings(result):
kind = classify(host, BRAND, OWNED, TYPO_LABELS)
if kind:
yield {"channel": channel, "position": position, "host": host, "kind": kind, "url": url, "title": title}
In the webhook handler, verify Webhook-Signature on the raw body, ignore "test": true, deduplicate by task.id, and pass response.result to candidates (webhooks). Store every candidate with the task id, query, market, device and time. Store the full response as evidence; add include.html to the payload if you want the raw page HTML as well.
Ad url values are often Google click-redirect links, so identify advertisers by domain, not url. An ad whose displayedUrl host differs from its domain is not necessarily abusive, but it is worth a look when either host resembles your brand.
Triage
Most candidates are not attacks. Score them before anyone spends time on them.
| Signal | Points |
|---|---|
| Seen in an ad on a branded query | +3 |
| Seen in organic top 10 for a login, support or payment query | +3 |
Exact typo or homoglyph candidate (typo, idn) |
+2 |
Combosquatting with a credential or payment word (login, verify, pay) |
+2 |
| Certificate issued in the last 30 days | +1 |
| Seen in 2+ checks | +1 |
| Title or ad text uses your brand name and “official” | +1 |
| Known reseller, affiliate, review site or your own defensive registration | −5 |
Then an analyst, in an isolated environment:
- Records registration data (WHOIS or RDAP), DNS records and certificate details.
- Captures the page, for example with a private urlscan.io scan, which its documentation says is “only visible to you” (visibility, checked 2026-09-17).
- Decides a category:
phishing,counterfeit,impersonation,parked,unrelated,legitimate. - Chooses the reporting route below.
Never enter credentials or payment details on a suspected site, and do not interact with it beyond capturing evidence.
Reporting routes
Choose by what the domain is doing. The routes below are those documented on the linked pages, checked 2026-09-17. Legal routes need counsel; this is not legal advice.
Phishing page
Google Safe Browsing has a “Report a Page to Google Safe Browsing” form (report). Also notify the hosting provider and registrar through their abuse contacts.
Counterfeit goods in ads
Google Ads prohibits “the sale or promotion for sale of counterfeit goods”, and its policy points to a Google Ads counterfeit complaint form; it states that a response to a valid complaint “may include removing the content or terminating the user’s account” (counterfeit goods policy).
Impersonation in ads
Google Ads’ misrepresentation policy prohibits ads that “make it seem like you’re supported by another brand, organization or government entity when you’re not” (misrepresentation policy).
Trademark use in ad text
Google Ads’ trademark policy restricts certain uses of trademarks in ad text, directs trademark owners to its “Report Content On Google” process, and does not restrict “using trademarks as keywords” (trademarks policy). A competitor bidding on your brand name is therefore not, by itself, a policy violation.
The domain itself
For trademark-based disputes over a domain name, ICANN’s Uniform Domain-Name Dispute-Resolution Policy requires the complainant to show that the domain “is identical or confusingly similar to a trademark or service mark in which the complainant has rights”, that the registrant has “no rights or legitimate interests” in it, and that it “has been registered and is being used in bad faith”; remedies are limited to cancellation or transfer (UDRP). Complaints go to an approved dispute-resolution service provider (ICANN UDRP overview).
Log every report with the date, route, reference number and evidence, then keep checking. A case is closed only when the domain stops appearing across several consecutive checks.
Metrics
| Metric | Definition |
|---|---|
| Exposed lookalikes | Distinct lookalike hosts seen in organic results, ads or AI Overview sources in a period |
| Ad exposure rate | Branded query checks with at least one lookalike ad ÷ branded query checks |
| Time to detection | First sighting in results − certificate log date or registration date, when known |
| Time to takedown | Last sighting − report date, for cases reported |
| Recurrence | Closed cases seen again within 30 days ÷ closed cases |
Cost
A Google Search async task is 3 credits for one page, plus 2 with include.aioverview. The example above is 5 queries × 2 countries × 2 devices = 20 tasks per run; four runs a day is 80 tasks, 240 credits a day, about 7,200 a month without AI Overview. dnstwist and crt.sh add no API cost. See pricing.
This API does not return WHOIS, DNS, certificate or page content data. It shows what Google results and ads displayed for a query, market and device.
Pitfalls
- Alerting on every permutation. Thousands of candidates exist; few are registered and fewer are exposed. Prioritise by exposure in results and ads.
- Forgetting your own registrations. Defensive registrations and marketing domains trigger matches. Keep the owned list current.
- Naive domain parsing. Split hosts with the Public Suffix List, and decode punycode before comparing.
- Single-device checks. Ads targeting mobile users are invisible to desktop-only checks.
- Acting before verifying. Resellers and affiliates can legitimately use your brand in a domain under an agreement. Check contracts before reporting.
The brand protection use case covers the wider pipeline for unauthorised ads, sellers and AI shopping answers, and OSINT tools for defenders covers the surrounding toolset. Request fields are on the Google Search engine page.
Questions
What is typosquatting?
Registering a domain that differs slightly from a real brand's domain, such as a missing letter, swapped letters, a lookalike character, an added word or a different top-level domain, to catch mistyped traffic or to impersonate the brand for phishing, fraud or counterfeit sales.
Why look for lookalike domains in search results instead of only in DNS?
Registration lists show which lookalikes exist; search results and ads show which ones are reaching people. A lookalike ranking for your brand name or advertising on it is actively exposed to customers and should be triaged first.
Is a competitor bidding on my brand name a typosquatting case?
Not by itself. Google Ads' trademark policy, checked 2026-09-17, does not restrict using trademarks as keywords, and restricts certain uses in ad text instead. A lookalike domain, impersonation or counterfeit offer is a different matter with its own reporting routes.
How do I report a lookalike domain?
Depending on what it does: report phishing pages to Google Safe Browsing, report ads under Google Ads policies such as counterfeit goods or misrepresentation, contact the registrar and hosting provider's abuse teams, and for trademark-based domain disputes consider the UDRP through an approved provider with legal counsel.
Can the API tell me who registered a domain?
No. It returns what Google results and ads showed, including the link, displayed URL and advertiser domain. Registration data, DNS records and page content come from other sources such as WHOIS or RDAP, DNS lookups and your analysts.