whisper.variants() — Lookalike Generation
Generate typosquat and homoglyph lookalikes of a domain, keep only the registered ones, and pivot the hits straight into a threat verdict.
On this page (5)
whisper.variants() — Lookalike Generation Documentation
whisper.variants() generates lookalike domains for a name you give it and, by default, returns only the ones registered in the graph. One call replaces the usual pipeline of an offline typosquat generator plus a bulk registration check. Because the generation and the existence check happen where the rest of the investigation lives, you can chain the hits straight into DNS resolution, routing, and threat-feed context in the same query.
The procedure takes the domain as its first argument. By default it filters to variants that exist as HOSTNAME nodes; pass a node label as the second argument to check a different node type, or false as the filter argument to return every generated variant, registered or not. Each row carries variant, method, exists, confidence and confidenceLabel.
Generation strategies
The domain is run through a set of generation algorithms: character omission, repetition, transposition, keyboard-adjacent replacement and insertion, homoglyph substitution, bitsquatting, TLD swap, and others. The method column names the strategy behind each row (OMISSION, TRANSPOSITION, HOMOGLYPH, BITSQUATTING, ...), and confidence ranks how plausible the variant is as a lookalike, so you can sort the shortlist before triaging it.
// Registered lookalikes of paypal.com, highest-confidence first
CALL whisper.variants("paypal.com")
YIELD variant, method, exists, confidence, confidenceLabel
WHERE exists
RETURN variant, method, confidence, confidenceLabel
ORDER BY confidence DESC
LIMIT 25
[
{"variant": "paypa1.com", "method": "HOMOGLYPH", "confidence": 1.0, "confidenceLabel": "high"},
{"variant": "paypai.com", "method": "HOMOGLYPH", "confidence": 1.0, "confidenceLabel": "high"},
{"variant": "paypan.com", "method": "BITSQUATTING", "confidence": 1.0, "confidenceLabel": "high"},
{"variant": "paytal.com", "method": "BITSQUATTING", "confidence": 1.0, "confidenceLabel": "high"}
]
Captured 2026-09-02, abridged. The registered set moves as names are registered and dropped.
What the exists flag means
exists: true means the variant is registered. It does not mean the variant is malicious. A parked typosquat and a live phishing page both show up as registered, so treat every hit as a candidate that still needs triage.
The unregistered side is useful too. Pass false as the filter argument and the full generated space comes back, which is the list worth reviewing for defensive registration before someone else stands one up.
CALL whisper.variants("paypal.com", false)
YIELD variant, method, exists
WHERE NOT exists
RETURN variant, method
LIMIT 10
In expression position
whisper.variants is also a function, so it works inside an expression as well as after CALL: size(whisper.variants("paypal.com")) counts the generated space, and slicing the list gives you the names without a YIELD. The function form returns the variant names only; use CALL when you need method, exists or confidence.
RETURN size(whisper.variants("paypal.com")) AS generated,
whisper.variants("paypal.com")[0..5] AS sample
Read
coveragebeforeband. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says. Full contract: Coverage — what we looked at.
Pivot the hits to a verdict
The standard triage read chains the generator into resolution and the reconciled verdict properties in one pass: which registered lookalikes are live, where they resolve, and which already carry a blocking verdict.
// Registered variants → where they resolve → reconciled verdict, all at once
CALL whisper.variants("paypal.com")
YIELD variant, method, exists
WHERE exists
WITH variant, method LIMIT 50
MATCH (h:HOSTNAME {name: variant})-[:RESOLVES_TO]->(ip:IPV4)
RETURN variant, method, ip.name AS resolves_to,
ip.verdictLevel AS ip_verdict, ip.verdictBlocking AS blocking
ORDER BY blocking DESC, ip_verdict DESC
LIMIT 25
For any single suspect, explain() returns the full evidence chain: score, level, the contributing feeds with their weights, and first/last-seen timestamps you can paste into a takedown ticket.
CALL explain("paypa1.com")
YIELD indicator, found, score, level, explanation, sources
RETURN indicator, found, score, level, explanation, sources
Keep the coverage caveat in mind: a NONE level is not proof the graph has never seen the name. Run the call above and paypa1.com returns NONE with a populated sources array, so read found, sources and explanation together, and ask whisper.assess the coverage question. Either way a lookalike can still be a brand-new phishing page. Fresh lookalikes routinely beat feed coverage, so a variant that resolves but has no verdict yet is often the one to look at first.
Where it fits
- The Brand Protection use case and its Lookalike Hunting recipes build the full loop on this procedure: surface scans, cluster expansion through co-hosting and shared nameservers, and certificate-transparency monitoring.
- To run the generate-and-triage loop in the browser without writing Cypher, use the Typosquat Scanner workflow.
- On the AI side, the same capability is available over MCP: call the procedure through the
querytool, or run thetyposquatworkflow withrun_workflow; see the MCP reference. - For the other procedures and when to prefer them over a hand-written traversal, see the procedures overview.