Skip to contentSkip navigation

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.

Procedures
On this page (4)

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 the variant, the method that produced it, an exists flag, and a confidence score with a 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, ...), and confidence ranks how plausible the variant is as a lookalike, so you can sort the shortlist before triaging it.

cypher · runnablegraph.whisper.securitySign in to run
// 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

json
[
  {"variant": "paypa1.com", "method": "HOMOGLYPH", "confidence": 0.9, "confidenceLabel": "high"},
  {"variant": "paypall.com", "method": "REPETITION", "confidence": 0.8, "confidenceLabel": "high"},
  {"variant": "papyal.com", "method": "TRANSPOSITION", "confidence": 0.7, "confidenceLabel": "medium"},
  {"variant": "aypal.com", "method": "OMISSION", "confidence": 0.7, "confidenceLabel": "medium"}
]

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.

Read coverage before band. 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. whisper.explain does not return coverage at all. 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.

cypher · runnablegraph.whisper.securitySign in to run
// 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.

cypher · runnablegraph.whisper.securitySign in to run
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 verdict means the score came back zero, not that the graph has never seen the name — run the call above and paypa1.com returns NONE with a non-empty sources array. Either way it 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 query tool, or run the typosquat workflow with run_workflow; see the MCP reference.
  • For the other procedures and when to prefer them over a hand-written traversal, see the procedures overview.