# Domain Impersonation Scanner — Typosquat & Phishing Finder (`cerridwen/domain-impersonation-scanner`) Actor

Find domains impersonating your brand. Generates typosquat, combosquat, homograph (punycode) and TLD-swap permutations, checks which are actually registered, then profiles each: mail servers, TLS, live pages, login forms and brand mentions. Returns a 0-100 risk score with takedown steps.

- **URL**: https://apify.com/cerridwen/domain-impersonation-scanner.md
- **Developed by:** [Cerridwen](https://apify.com/cerridwen) (community)
- **Categories:** Developer tools, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / domain scanned

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Domain Impersonation Scanner — Typosquat & Phishing Domain Finder

Find the domains impersonating your brand before your customers do.

Give it a domain you own. It generates thousands of realistic impersonation
candidates — typos, combosquats, TLD swaps, Unicode homographs — checks which
ones are **actually registered**, then profiles each one to answer the question
that matters: *can this domain be used to attack us right now?*

A lookalike with a working mail server can send fake invoices to your customers
today. One that merely exists cannot. This actor tells the two apart.

### Why this instead of a free typosquat generator

Tools like `dnstwist` list permutations. That is the easy half. The hard half is
deciding which of the 60 registered lookalikes deserve your lawyer's attention
this week — and that needs mail, TLS, page content and parking signals, scored
together.

- **Capability-weighted risk score (0-100)** — MX records and credential forms
  outweigh cosmetic similarity
- **Explains every score** — each finding lists the exact reasons and any
  mitigating factors, so you can audit it rather than trust it
- **Separates threats from your own defensive registrations** — a lookalike
  that redirects to you is scored *down*, not up
- **Resolver-hardened** — see [Accuracy](#accuracy) below
- **Actionable output** — each finding carries concrete takedown steps

### What it detects

| Technique | Example for `example.com` | Why it matters |
|---|---|---|
| Combosquatting | `example-login.com`, `secure-example.com` | The most common credential-phishing pattern |
| TLD swap | `example.co`, `example.io`, `example.ru` | Cheapest lookalike to register |
| Unicode homograph | `exаmple.com` (Cyrillic а → `xn--exmple-4nf.com`) | Visually **identical** in most browsers and mail clients |
| ASCII homoglyph | `exarnple.com` (`rn` for `m`), `examp1e.com` | Passes a quick glance |
| Typos | `exmaple.com`, `examle.com`, `exampple.com` | Catches fat-finger traffic |
| Hyphenation | `ex-ample.com` | Common in ad-fraud and resale |
| Subdomain | `ex.ample.com` | Exploits the dot as a visual separator |
| Addition | `examples.com` | Adjacent-brand confusion |
| Bitsquatting | single bit-flip variants | Opt-in; low yield, high volume |

### Signals collected per registered lookalike

**DNS** — A/AAAA addresses, MX records with mail-provider identification
(Google Workspace, Microsoft 365, Zoho, ImprovMX…), nameservers, SPF, DMARC
policy, domain-parking network detection

Mail is classified by *how it came to exist*, not merely that it exists:

| Capability | Examples | Weight |
|---|---|---|
| `mailbox` | Google Workspace, Microsoft 365, self-hosted | Full |
| `forwarding` | Namecheap eforward, ImprovMX, Cloudflare Email Routing | Reduced |
| `parking` | `park-mx.above.com`, NameBright | None |

The distinction matters: registering a domain at Namecheap switches on free
forwarding automatically. Scoring that like a deliberately provisioned mailbox
put five unrelated domains at `HIGH` in a single scan. Only a real mailbox
triggers the invoice-fraud recommendation.

**TLS** — certificate presence, issuer, validity start, certificate age (a
certificate issued last week on a lookalike is a setup-in-progress signal)

**HTTP** — status, final URL after redirects, page title, password-field
detection, brand mentions in page content, "for sale" parking pages

### Risk scoring

| Level | Score | Typical meaning |
|---|---|---|
| `CRITICAL` | 70-100 | Impersonation evidence **plus** the capability to act on it |
| `HIGH` | 50-69 | Registered with working mail or a live site |
| `MEDIUM` | 30-49 | Registered and resolving, limited capability |
| `LOW` | 15-29 | Registered, mostly dormant |
| `INFO` | 0-14 | Parked, for sale, or redirecting to you |

**Capability is not intent.** Mail servers, TLS and SPF describe how much
damage a domain *could* do; they say nothing about whether it is pretending to
be you. An established unrelated business one keystroke from your name has the
same capability profile as a well-provisioned phishing domain.

So `CRITICAL` requires at least one piece of impersonation evidence — the page
references your brand, hosts a login form, or uses confusable Unicode. Without
any of those the score is capped at `HIGH` and the finding says so explicitly,
rather than telling you to file a takedown against a real company.

Findings that redirect to your own domain are scored down by 45 points, as are
domains held at a corporate brand-protection registrar (MarkMonitor, CSC, Com
Laude) — nobody phishes from those, so a hit there means you already own it.

### Re-scanning

Brand protection is a standing task, not a one-off. Pass the domains you have
already cleared — localised sites, defensive registrations, partner domains —
in `allowlist`, and they drop out of findings into an `allowlisted` count. A
monthly re-scan then surfaces only what changed.

```json
{
    "domains": ["yourbrand.com"],
    "allowlist": ["yourbrand.ca", "yourbrand.de", "yourbrand.fr"],
    "minRiskScore": 30
}
```

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `domains` | string\[] | *required* | Domains you want to protect. Bare domains or full URLs both work |
| `techniques` | string\[] | all but bitsquatting | Which permutation families to generate |
| `maxPermutations` | integer | `2500` | Ceiling per domain; low-yield techniques are dropped first |
| `checkHttp` | boolean | `true` | Fetch each registered lookalike's web page |
| `allowlist` | string\[] | `[]` | Domains you have already cleared as your own; excluded from findings |
| `minRiskScore` | integer | `15` | Omit findings below this score; `0` reports everything |
| `maxDomains` | integer | `10` | Targets per run |
| `resolvers` | string\[] | `1.1.1.1`, `8.8.8.8`, `9.9.9.9` | Nameservers to query |
| `dnsConcurrency` | integer | `60` | Parallel DNS queries |
| `dnsTimeoutSeconds` | number | `4` | Per-query DNS timeout |
| `httpTimeoutSeconds` | number | `8` | Per-request page timeout |

#### Example input

```json
{
    "domains": ["yourbrand.com"],
    "checkHttp": true,
    "minRiskScore": 30
}
```

### Output

The dataset contains one row per finding (`recordType: "finding"`), sorted
highest-risk first, plus one summary row per target (`recordType: "summary"`)
so a clean scan never produces a confusingly empty dataset. The full summary is
also written to the key-value store as `SCAN_SUMMARY`.

```json
{
    "recordType": "finding",
    "target": "apify.com",
    "impersonatingDomain": "apify.fr",
    "punycode": null,
    "technique": "tld-swap",
    "riskScore": 100,
    "riskLevel": "CRITICAL",
    "reasons": [
        "Domain is registered and resolves to an IP",
        "Has MX records — can send or receive mail as your brand",
        "Has a valid TLS certificate — renders with a padlock",
        "SPF record published — mail sending is configured",
        "Serves a live web page",
        "Page content references your brand"
    ],
    "mitigatingFactors": [],
    "recommendations": [
        "Content references your brand without redirecting to you — capture evidence (screenshot plus WHOIS) and open a trademark complaint with the registrar.",
        "Mail is configured on this lookalike. Warn finance and AP teams about invoice fraud, and add the domain to your mail gateway's block list."
    ],
    "hasMailServer": true,
    "mxRecords": ["apify-fr.mail.protection.outlook.com"],
    "mailProvider": "Microsoft 365",
    "spf": "v=spf1 include:_spf-eu.ionos.com include:spf.protection.outlook.com ~all",
    "hasTls": true,
    "tlsIssuer": "Sectigo Limited",
    "tlsAgeDays": 160,
    "httpStatus": 200,
    "finalUrl": "https://apify.fr/",
    "mentionsBrand": true,
    "isParked": false
}
```

Three dataset views are configured: **Impersonation findings** (default),
**High risk only**, and **Technical detail**.

### Accuracy

Two failure modes produce phantom findings in naive typosquat tools, and both
are handled here.

**NXDOMAIN hijacking.** Many ISP, corporate and consumer-filtering resolvers
answer a nonexistent domain with a sentinel IP (commonly `0.0.0.0`) instead of
an `NXDOMAIN` error. A scanner trusting the system resolver would report every
single unregistered permutation as a live lookalike. This actor queries public
resolvers directly and never reads the host's resolver configuration.

**Wildcard TLDs and resolver quirks.** Before each scan the actor resolves two
guaranteed-nonexistent names — one under `.com` and one under the target's own
suffix — and treats any address that comes back as noise for the remainder of
the scan. That catches registry-level wildcards as well as resolver lies.

Domains with MX records but no A record are still reported: a lookalike set up
purely for invoice fraud often has no website at all. A **null MX** (RFC 7505,
the record `0 .`) is the opposite case — the domain is explicitly declaring it
accepts no mail — and is never counted as mail capability.

**Self-references do not count as brand mentions.** A parking page for
`yourbrand.nl` naturally contains the string `yourbrand`, and `aayourbrand.com`
contains it as a substring. Matching is word-bounded and strips the
lookalike's own hostname first, so neither fires.

**Parked domains are detected three ways** — nameservers, mail exchangers, and
the host a page redirects to. Relying on any single one is fragile: a DNS
lookup that times out under load would otherwise flip a parked domain into an
active threat.

**Bot-challenge pages are marked, not interpreted.** When a fetch returns a
Cloudflare interstitial the real page was never seen, so no content signal is
derived from it and the finding says the check was inconclusive.

For-sale detection runs against visible page text rather than raw HTML. Meta
tags and inline scripts routinely contain parking-page phrasing — a WordPress
maintenance plugin advertises itself as "coming soon" in a `generator` tag —
without the page being parked at all.

### Use cases

- **Brand protection** — continuous monitoring for trademark and takedown teams
- **Anti-phishing** — find credential-harvesting pages before the campaign lands
- **Invoice-fraud prevention** — surface lookalikes with working mail servers
- **M\&A and vendor due diligence** — check a target company's exposure
- **Security audits** — a concrete, evidence-backed deliverable for clients

### Performance

A typical five-character brand generates ~300 candidates and completes in about
a minute including page fetches. Longer names generate more. The pipeline is a
funnel — thousands of cheap DNS lookups, then TLS and HTTP probes only against
the small subset that is actually registered — so cost scales with the number
of *real* lookalikes rather than the number of permutations.

Set `checkHttp: false` for a DNS-only scan that runs several times faster.

### Local development

```bash
pip install -r requirements.txt
python3 test_local.py yourbrand.com
python3 test_local.py yourbrand.com --no-http --max 800
python3 test_local.py yourbrand.com --json > findings.json
```

### Monetisation setup

The actor emits two pay-per-event charges:

| Event | When |
|---|---|
| `actor-start` | Once per run |
| `domain-scanned` | Once per target domain |

Billing follows the work performed, not the number of findings — nothing here
rewards reporting noise. Configure both events in the Apify Console under
**Monetization**; until you do, the charge calls degrade silently and runs stay
free.

### Notes

This actor performs passive reconnaissance only: DNS queries and ordinary
unauthenticated HTTP GET requests to publicly reachable hosts. It sends no
email, submits no forms, and attempts no authentication.

# Actor input Schema

## `domains` (type: `array`):

Your own domains (or your client's). Accepts bare domains or full URLs — 'example.com', 'www.example.com' and 'https://example.com/pricing' all work.

## `techniques` (type: `array`):

Which impersonation techniques to generate. Leave empty for the recommended set (everything except bitsquatting, which has a very low hit rate).

## `maxPermutations` (type: `integer`):

Upper bound on generated candidates. Lower-yield techniques are dropped first, so reducing this loses the least useful candidates.

## `checkHttp` (type: `boolean`):

Fetch each registered lookalike over HTTP to detect login forms, brand mentions, parking pages and redirects back to you. Turn off for a faster DNS-only scan.

## `allowlist` (type: `array`):

Domains you have already confirmed as your own — defensive registrations, localised sites, partner domains. They are excluded from findings and counted separately, so a monthly re-scan only surfaces what is new.

## `minRiskScore` (type: `integer`):

Findings scoring below this are omitted. 0 reports every registered lookalike, including parked ones and your own defensive registrations.

## `maxDomains` (type: `integer`):

Maximum target domains to scan in a single run.

## `resolvers` (type: `array`):

Nameservers to query. Defaults to 1.1.1.1, 8.8.8.8 and 9.9.9.9. The system resolver is deliberately not used — filtering resolvers answer NXDOMAIN with a sentinel IP, which would report every unregistered permutation as a live lookalike.

## `dnsConcurrency` (type: `integer`):

Parallel DNS queries. Raise for faster scans; lower if your resolver rate-limits.

## `dnsTimeoutSeconds` (type: `number`):

Per-query DNS timeout.

## `httpTimeoutSeconds` (type: `number`):

Per-request timeout for the web page probe.

## Actor input object example

```json
{
  "domains": [
    "apify.com"
  ],
  "techniques": [],
  "maxPermutations": 2500,
  "checkHttp": true,
  "allowlist": [],
  "minRiskScore": 15,
  "maxDomains": 10,
  "resolvers": [],
  "dnsConcurrency": 60,
  "dnsTimeoutSeconds": 4,
  "httpTimeoutSeconds": 8
}
```

# Actor output Schema

## `findings` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cerridwen/domain-impersonation-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": ["apify.com"] }

# Run the Actor and wait for it to finish
run = client.actor("cerridwen/domain-impersonation-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "apify.com"
  ]
}' |
apify call cerridwen/domain-impersonation-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cerridwen/domain-impersonation-scanner"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/PLQ3i9Qoa1dsz93la/builds/LKfxl08w84t0fD942/openapi.json
