@burtthecoder/mcp-dnstwist 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -154
- package/build/index.js +152 -43
- package/build/validate.js +28 -1
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,181 +1,123 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<a href="https://
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="MCP dnstwist: typosquatting detection for AI agents. Find lookalike, homoglyph and phishing domains with dnstwist over the Model Context Protocol." src="docs/assets/banner.webp" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@burtthecoder/mcp-dnstwist"><img alt="npm version" src="https://img.shields.io/npm/v/@burtthecoder/mcp-dnstwist"></a>
|
|
7
|
+
<a href="https://github.com/w0h1v/mcp-dnstwist/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/w0h1v/mcp-dnstwist/actions/workflows/ci.yml/badge.svg"></a>
|
|
8
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
9
|
+
<img alt="Node 20.3 or newer" src="https://img.shields.io/badge/node-%E2%89%A520.3-339933">
|
|
10
|
+
<a href="https://registry.modelcontextprotocol.io"><img alt="MCP Registry" src="https://img.shields.io/badge/MCP-Registry-7c3aed"></a>
|
|
11
|
+
<a href="https://smithery.ai/server/@burtthecoder/mcp-dnstwist"><img alt="Smithery" src="https://img.shields.io/badge/Smithery-listed-ff5601"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="#quickstart">Quickstart</a> ·
|
|
16
|
+
<a href="#the-tool">Tool</a> ·
|
|
17
|
+
<a href="#security">Security</a> ·
|
|
18
|
+
<a href="#troubleshooting">Troubleshooting</a> ·
|
|
19
|
+
<a href="https://w0h1v.github.io/mcp-dnstwist/">Docs</a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
**mcp-dnstwist** is an [MCP](https://modelcontextprotocol.io) server that lets AI agents run [dnstwist](https://github.com/elceef/dnstwist) to find domains that imitate yours: typos, homoglyphs, bit flips, swapped TLDs and more. For each registered lookalike it can report DNS records, whether its mail server could intercept your email, WHOIS registration details, and how closely its web page copies yours.
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> Independent project, not affiliated with dnstwist. Only scan domains you own or are authorized to assess.
|
|
26
|
+
|
|
27
|
+
> [!IMPORTANT]
|
|
28
|
+
> Versions up to 1.0.4 have a command injection vulnerability ([GHSA-p4jr-h3rj-2rw4](https://github.com/w0h1v/mcp-dnstwist/security/advisories/GHSA-p4jr-h3rj-2rw4)) and are deprecated. Use 1.0.5 or later; `npx -y @burtthecoder/mcp-dnstwist` always fetches the latest.
|
|
29
|
+
|
|
30
|
+
## Highlights
|
|
31
|
+
|
|
32
|
+
- **Lookalike discovery.** All of dnstwist's permutation algorithms, or only the ones you pick.
|
|
33
|
+
- **Threat signals.** DNS records, open mail relays that could intercept email (`mxcheck`), WHOIS creation date and registrar, HTTP and SMTP banners, and web-page similarity (ssdeep or TLSH) to spot cloned phishing sites.
|
|
34
|
+
- **Runs your way.** A local dnstwist install, the official dnstwist Docker image, or a self-contained mcp-dnstwist image.
|
|
35
|
+
- **Hardened.** No shell, strict input validation, and time limits that stop and clean up long scans.
|
|
36
|
+
- **Tested.** End-to-end tests with fake `docker` and `dnstwist` executables run in CI on Node 20, 22 and 24.
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
flowchart LR
|
|
42
|
+
A["AI agent<br/>(Claude, Codex, Gemini, VS Code)"] -->|"MCP over stdio"| S["mcp-dnstwist"]
|
|
43
|
+
S -->|"validated argv, no shell,<br/>time limit"| D["dnstwist<br/>(local or Docker)"]
|
|
44
|
+
D -->|"DNS, WHOIS, HTTP, SMTP"| I["Lookalike domains"]
|
|
45
|
+
D -->|"JSON"| S
|
|
46
|
+
S -->|"results"| A
|
|
32
47
|
```
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
1. Install Docker:
|
|
36
|
-
- macOS: Install [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
|
37
|
-
- Linux: Follow the [Docker Engine installation guide](https://docs.docker.com/engine/install/)
|
|
49
|
+
## Quickstart
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install -g @burtthecoder/mcp-dnstwist
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
3. Add to your Claude Desktop configuration file:
|
|
45
|
-
```json
|
|
46
|
-
{
|
|
47
|
-
"mcpServers": {
|
|
48
|
-
"dnstwist": {
|
|
49
|
-
"command": "mcp-dnstwist"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
```
|
|
51
|
+
**Requirements:** Node.js 20.3 or newer, and either dnstwist (`pipx install "dnstwist[full]"`) or Docker. No API key.
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
53
|
+
```sh
|
|
54
|
+
# Claude Code
|
|
55
|
+
claude mcp add --transport stdio dnstwist -- npx -y @burtthecoder/mcp-dnstwist
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
# Codex CLI
|
|
58
|
+
codex mcp add dnstwist -- npx -y @burtthecoder/mcp-dnstwist
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
# Gemini CLI
|
|
61
|
+
gemini mcp add dnstwist npx -y @burtthecoder/mcp-dnstwist
|
|
62
|
+
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
For Claude Desktop, VS Code, Docker, Smithery and running from source, see [Connect a client](docs/clients.md).
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
git clone <repository_url>
|
|
68
|
-
cd mcp-dnstwist
|
|
69
|
-
npm install
|
|
70
|
-
npm run build
|
|
71
|
-
```
|
|
66
|
+
Then ask your agent: *"Which lookalike domains of example.com are registered, and could any of them intercept email?"* More prompts are in [example workflows](docs/examples.md).
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
```json
|
|
75
|
-
{
|
|
76
|
-
"mcpServers": {
|
|
77
|
-
"dnstwist": {
|
|
78
|
-
"command": "node",
|
|
79
|
-
"args": ["/absolute/path/to/mcp-dnstwist/build/index.js"]
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
68
|
+
## The tool
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- **Domain Fuzzing**: Generate domain permutations using various algorithms
|
|
88
|
-
- **Registration Check**: Verify if permutated domains are registered
|
|
89
|
-
- **DNS Analysis**: Check A, AAAA, MX, and NS records
|
|
90
|
-
- **Web Presence**: Capture HTTP banner information
|
|
91
|
-
- **WHOIS Data**: Retrieve registration dates and registrar information
|
|
92
|
-
- **Phishing Detection**: Generate fuzzy hashes of web pages
|
|
93
|
-
- **Configurable**: Custom DNS servers and parallel processing
|
|
94
|
-
- **Multiple Formats**: Support for json, csv, and list output formats
|
|
95
|
-
|
|
96
|
-
## Tools
|
|
97
|
-
|
|
98
|
-
### Domain Fuzzing Tool
|
|
99
|
-
- Name: `fuzz_domain`
|
|
100
|
-
- Description: Generate and analyze domain permutations to detect potential typosquatting, phishing, and brand impersonation
|
|
101
|
-
- Parameters:
|
|
102
|
-
* `domain` (required): Domain name to analyze (e.g., example.com)
|
|
103
|
-
* `nameservers` (optional, default: "1.1.1.1"): Comma-separated DNS servers, up to 10. Each must be an IPv4 address or a DNS-over-HTTPS URL such as `https://cloudflare-dns.com/dns-query` (the forms dnstwist accepts). DNS-over-HTTPS is much slower: a DoH-only scan can take over 10 minutes, so include an IPv4 resolver unless you need DoH
|
|
104
|
-
* `threads` (optional, default: 50): Number of threads for parallel processing (1-100)
|
|
105
|
-
* `format` (optional, default: "json"): Output format (json, csv, list)
|
|
106
|
-
* `registered_only` (optional, default: true): Show only registered domains
|
|
107
|
-
* `mxcheck` (optional, default: true): Check for MX records
|
|
108
|
-
* `ssdeep` (optional, default: false): Generate fuzzy hashes of web pages
|
|
109
|
-
* `banners` (optional, default: true): Capture HTTP banner information
|
|
110
|
-
|
|
111
|
-
Example:
|
|
112
|
-
```json
|
|
113
|
-
{
|
|
114
|
-
"domain": "example.com",
|
|
115
|
-
"nameservers": "1.1.1.1,8.8.8.8",
|
|
116
|
-
"threads": 50,
|
|
117
|
-
"format": "json",
|
|
118
|
-
"registered_only": true,
|
|
119
|
-
"mxcheck": true,
|
|
120
|
-
"banners": true
|
|
121
|
-
}
|
|
122
|
-
```
|
|
70
|
+
One tool, `fuzz_domain`. Full parameters and real sample output are in the [tool reference](docs/tools.md).
|
|
123
71
|
|
|
124
|
-
|
|
72
|
+
| Parameter | Default | What it does |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `domain` | *(required)* | The domain to imitate, for example `example.com` |
|
|
75
|
+
| `registered_only` | `true` | Only return lookalikes that exist |
|
|
76
|
+
| `fuzzers` | all | Limit to some algorithms, for example `["homoglyph", "bitsquatting"]` |
|
|
77
|
+
| `mxcheck` | `true` | Flag mail servers that would accept email for any address |
|
|
78
|
+
| `banners` | `true` | Capture HTTP and SMTP banners |
|
|
79
|
+
| `whois` | `false` | Registration date and registrar |
|
|
80
|
+
| `lsh` | off | Web-page similarity to the original: `ssdeep` or `tlsh` |
|
|
81
|
+
| `all_records` | `false` | All DNS records, not only the first of each type |
|
|
82
|
+
| `nameservers` | `1.1.1.1` | Up to 10 IPv4 addresses or `https://<host>/dns-query` URLs |
|
|
83
|
+
| `threads` | `50` | Parallel lookups, 1 to 100 |
|
|
84
|
+
| `format` | `json` | `json`, `csv` or `list` |
|
|
85
|
+
|
|
86
|
+
### Settings
|
|
125
87
|
|
|
126
88
|
| Variable | Default | Description |
|
|
127
89
|
|---|---|---|
|
|
128
|
-
| `
|
|
90
|
+
| `DNSTWIST_RUNNER` | `auto` | `local` (dnstwist on `PATH`), `docker` (the `elceef/dnstwist` image), or `auto` (local if available) |
|
|
91
|
+
| `DNSTWIST_TIMEOUT_SECONDS` | `600` | Longest a scan may run. On timeout, cancellation or shutdown the scan is stopped and cleaned up |
|
|
129
92
|
|
|
130
|
-
|
|
93
|
+
## Security
|
|
131
94
|
|
|
132
|
-
|
|
95
|
+
The tool's arguments come from an AI model, which content it reads can steer, so the server treats them as hostile:
|
|
133
96
|
|
|
134
|
-
|
|
97
|
+
- **No shell.** dnstwist starts with `execFile` and an argument array; a test fails the build if a shell-based API appears.
|
|
98
|
+
- **Strict validation.** Domains must be hostnames, nameservers IPv4 or DoH URLs, numbers and names from fixed sets. No value can start with `-`.
|
|
99
|
+
- **Bounded runs.** Time limits, and cleanup of processes and containers on timeout, cancellation and shutdown.
|
|
135
100
|
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
docker --version
|
|
139
|
-
docker ps
|
|
140
|
-
```
|
|
101
|
+
The Docker runner needs access to the Docker daemon (root-equivalent on most systems). The local runner or the self-contained image avoid that. Details in the [security model](docs/security.md). Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).
|
|
141
102
|
|
|
142
|
-
|
|
143
|
-
- Ensure your user has permissions to run Docker commands
|
|
144
|
-
- On Linux, add your user to the docker group: `sudo usermod -aG docker $USER`
|
|
145
|
-
|
|
146
|
-
### Common Issues
|
|
147
|
-
|
|
148
|
-
1. DNS resolution problems:
|
|
149
|
-
- Verify DNS servers are accessible
|
|
150
|
-
- Try alternative DNS servers (e.g., 8.8.8.8)
|
|
151
|
-
- Check for rate limiting or blocking
|
|
152
|
-
|
|
153
|
-
2. Performance issues:
|
|
154
|
-
- Adjust thread count based on system capabilities
|
|
155
|
-
- Consider network bandwidth and latency
|
|
156
|
-
- Monitor DNS server response times
|
|
157
|
-
|
|
158
|
-
3. After fixing any issues:
|
|
159
|
-
- Save the configuration file
|
|
160
|
-
- Restart Claude Desktop
|
|
103
|
+
## Troubleshooting
|
|
161
104
|
|
|
162
|
-
|
|
105
|
+
- **"Neither dnstwist nor Docker is available":** install dnstwist (`pipx install "dnstwist[full]"`) or start Docker. Desktop clients may not see your shell's `PATH`.
|
|
106
|
+
- **Slow on Apple Silicon:** the dnstwist Docker image is amd64-only. Install dnstwist locally; it's used automatically.
|
|
107
|
+
- **"did not finish within N seconds":** include an IPv4 nameserver (DNS-over-HTTPS is slow), turn off `whois`, `lsh` or `banners`, or raise `DNSTWIST_TIMEOUT_SECONDS`.
|
|
163
108
|
|
|
164
|
-
|
|
109
|
+
More in the [troubleshooting guide](docs/troubleshooting.md).
|
|
165
110
|
|
|
166
|
-
|
|
167
|
-
- "Failed to parse dnstwist output": Check if the domain is valid and the format is correct
|
|
168
|
-
- "Error executing dnstwist": Check Docker logs and ensure proper permissions
|
|
169
|
-
- "DNS server not responding": Verify DNS server accessibility and try alternative servers
|
|
111
|
+
## Development
|
|
170
112
|
|
|
171
|
-
|
|
113
|
+
```sh
|
|
114
|
+
git clone https://github.com/w0h1v/mcp-dnstwist && cd mcp-dnstwist
|
|
115
|
+
npm ci
|
|
116
|
+
npm test # fake docker and dnstwist executables: no Docker or network needed
|
|
117
|
+
```
|
|
172
118
|
|
|
173
|
-
|
|
174
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
175
|
-
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
176
|
-
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
177
|
-
5. Open a Pull Request
|
|
119
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and the [changelog](CHANGELOG.md).
|
|
178
120
|
|
|
179
121
|
## License
|
|
180
122
|
|
|
181
|
-
|
|
123
|
+
MIT. See [LICENSE](LICENSE).
|
package/build/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } f
|
|
|
5
5
|
import { execFile } from 'child_process';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
|
-
import { validateFuzzArgs } from './validate.js';
|
|
9
|
-
// execFile runs docker directly with an argument array. No shell is involved,
|
|
8
|
+
import { FUZZERS, LSH_ALGORITHMS, validateFuzzArgs } from './validate.js';
|
|
9
|
+
// execFile runs docker (or dnstwist) directly with an argument array. No shell is involved,
|
|
10
10
|
// so tool input can never be interpreted as shell syntax (CWE-78, issue #13).
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
const DOCKER_IMAGE = 'elceef/dnstwist';
|
|
13
13
|
// A scan that runs longer than this is stopped and its container removed.
|
|
14
|
-
// Slow resolvers (DNS-over-HTTPS) and
|
|
14
|
+
// Slow resolvers (DNS-over-HTTPS), LSH and banners can take many minutes.
|
|
15
15
|
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
16
16
|
function scanTimeoutSeconds() {
|
|
17
17
|
const raw = process.env.DNSTWIST_TIMEOUT_SECONDS;
|
|
@@ -25,20 +25,36 @@ function scanTimeoutSeconds() {
|
|
|
25
25
|
return seconds;
|
|
26
26
|
}
|
|
27
27
|
const TIMEOUT_SECONDS = scanTimeoutSeconds();
|
|
28
|
+
const RUNNER_SETTINGS = ['docker', 'local', 'auto'];
|
|
29
|
+
// docker: run the elceef/dnstwist image. local: run a dnstwist on PATH.
|
|
30
|
+
// auto (default): local when a dnstwist executable works, otherwise docker.
|
|
31
|
+
function runnerSetting() {
|
|
32
|
+
const raw = process.env.DNSTWIST_RUNNER;
|
|
33
|
+
if (raw === undefined || raw === '')
|
|
34
|
+
return 'auto';
|
|
35
|
+
if (!RUNNER_SETTINGS.includes(raw)) {
|
|
36
|
+
console.error(`DNSTWIST_RUNNER must be one of ${RUNNER_SETTINGS.join(', ')}, got ${JSON.stringify(raw)}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return raw;
|
|
40
|
+
}
|
|
41
|
+
const RUNNER_SETTING = runnerSetting();
|
|
28
42
|
class DnstwistServer {
|
|
29
43
|
constructor() {
|
|
30
|
-
// Scans in progress:
|
|
31
|
-
|
|
44
|
+
// Scans in progress: id -> controller that stops the child process, plus the
|
|
45
|
+
// container name when the docker runner started one.
|
|
46
|
+
this.runningScans = new Map();
|
|
32
47
|
this.server = new Server({ name: 'dnstwist-server', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
33
48
|
this.setupToolHandlers();
|
|
34
49
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
35
50
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
36
51
|
process.on(signal, async () => {
|
|
37
52
|
// Don't leave scans running after the server is gone: stop each
|
|
38
|
-
// in-flight
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// in-flight child process first, then remove any docker container.
|
|
54
|
+
const scans = [...this.runningScans.values()];
|
|
55
|
+
for (const scan of scans)
|
|
56
|
+
scan.stop.abort();
|
|
57
|
+
await Promise.all(scans.flatMap((scan) => (scan.container ? [this.removeContainer(scan.container)] : [])));
|
|
42
58
|
await this.server.close();
|
|
43
59
|
process.exit(0);
|
|
44
60
|
});
|
|
@@ -52,10 +68,28 @@ class DnstwistServer {
|
|
|
52
68
|
async execCommand(file, args, options = {}) {
|
|
53
69
|
console.error('Executing:', file, JSON.stringify(args));
|
|
54
70
|
try {
|
|
55
|
-
const
|
|
71
|
+
const { killOnAbort, ...execOptions } = options;
|
|
72
|
+
const pending = execFileAsync(file, args, {
|
|
56
73
|
maxBuffer: 10 * 1024 * 1024,
|
|
57
|
-
...
|
|
74
|
+
...execOptions,
|
|
75
|
+
// execFile's own `signal` support always sends SIGTERM; when the process
|
|
76
|
+
// must be killed outright, handle the abort here instead.
|
|
77
|
+
...(killOnAbort ? { signal: undefined } : {})
|
|
58
78
|
});
|
|
79
|
+
const abort = () => pending.child.kill('SIGKILL');
|
|
80
|
+
if (killOnAbort && options.signal) {
|
|
81
|
+
if (options.signal.aborted)
|
|
82
|
+
abort();
|
|
83
|
+
else
|
|
84
|
+
options.signal.addEventListener('abort', abort, { once: true });
|
|
85
|
+
}
|
|
86
|
+
let result;
|
|
87
|
+
try {
|
|
88
|
+
result = await pending;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
options.signal?.removeEventListener('abort', abort);
|
|
92
|
+
}
|
|
59
93
|
console.error('Command output:', result.stdout);
|
|
60
94
|
if (result.stderr)
|
|
61
95
|
console.error('Command stderr:', result.stderr);
|
|
@@ -82,25 +116,67 @@ class DnstwistServer {
|
|
|
82
116
|
}
|
|
83
117
|
console.error(`Could not confirm removal of container ${name}`);
|
|
84
118
|
}
|
|
119
|
+
// Decided once. A failed check is not cached, so a later call can retry.
|
|
120
|
+
getRunner() {
|
|
121
|
+
this.runnerPromise ?? (this.runnerPromise = this.selectRunner().catch((error) => {
|
|
122
|
+
this.runnerPromise = undefined;
|
|
123
|
+
throw error;
|
|
124
|
+
}));
|
|
125
|
+
return this.runnerPromise;
|
|
126
|
+
}
|
|
127
|
+
async checkLocal() {
|
|
128
|
+
try {
|
|
129
|
+
await this.execCommand('dnstwist', ['--version']);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error('The dnstwist executable was not found or does not work (`dnstwist --version` failed). ' +
|
|
133
|
+
'Install it (for example `pipx install "dnstwist[full]"`) or set DNSTWIST_RUNNER=docker.');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async selectRunner() {
|
|
137
|
+
if (RUNNER_SETTING === 'docker')
|
|
138
|
+
return 'docker';
|
|
139
|
+
if (RUNNER_SETTING === 'local') {
|
|
140
|
+
await this.checkLocal();
|
|
141
|
+
console.error('Using the local dnstwist executable (DNSTWIST_RUNNER=local)');
|
|
142
|
+
return 'local';
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await this.checkLocal();
|
|
146
|
+
console.error('Using the local dnstwist executable (DNSTWIST_RUNNER=auto)');
|
|
147
|
+
return 'local';
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
console.error('No working dnstwist executable on PATH; using Docker (DNSTWIST_RUNNER=auto)');
|
|
151
|
+
return 'docker';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async checkDocker() {
|
|
155
|
+
console.error('Checking Docker...');
|
|
156
|
+
try {
|
|
157
|
+
await this.execCommand('docker', ['--version']);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throw new Error('Docker is not installed or not running. Please install Docker and try again.');
|
|
161
|
+
}
|
|
162
|
+
console.error('Checking if dnstwist image exists...');
|
|
163
|
+
try {
|
|
164
|
+
await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
|
|
165
|
+
console.error('Dnstwist image found');
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('Dnstwist image not found, pulling...');
|
|
169
|
+
await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
|
|
170
|
+
console.error('Dnstwist image pulled successfully');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
85
173
|
async ensureSetup() {
|
|
86
174
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
throw new Error('Docker is not installed or not running. Please install Docker and try again.');
|
|
93
|
-
}
|
|
94
|
-
console.error('Checking if dnstwist image exists...');
|
|
95
|
-
try {
|
|
96
|
-
await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
|
|
97
|
-
console.error('Dnstwist image found');
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
console.error('Dnstwist image not found, pulling...');
|
|
101
|
-
await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
|
|
102
|
-
console.error('Dnstwist image pulled successfully');
|
|
103
|
-
}
|
|
175
|
+
const runner = await this.getRunner();
|
|
176
|
+
// Docker is re-checked per call: the image or daemon can disappear.
|
|
177
|
+
if (runner === 'docker')
|
|
178
|
+
await this.checkDocker();
|
|
179
|
+
return runner;
|
|
104
180
|
}
|
|
105
181
|
catch (error) {
|
|
106
182
|
console.error('Setup failed:', error);
|
|
@@ -127,7 +203,7 @@ class DnstwistServer {
|
|
|
127
203
|
},
|
|
128
204
|
threads: {
|
|
129
205
|
type: 'number',
|
|
130
|
-
description: 'Number of
|
|
206
|
+
description: 'Number of parallel lookups (1-100)',
|
|
131
207
|
minimum: 1,
|
|
132
208
|
maximum: 100,
|
|
133
209
|
default: 50
|
|
@@ -145,17 +221,40 @@ class DnstwistServer {
|
|
|
145
221
|
},
|
|
146
222
|
mxcheck: {
|
|
147
223
|
type: 'boolean',
|
|
148
|
-
description: 'Check for
|
|
224
|
+
description: 'Check whether each mail server would accept email for any address, so it could intercept mail sent to typos of your domain',
|
|
149
225
|
default: true
|
|
150
226
|
},
|
|
151
227
|
ssdeep: {
|
|
152
228
|
type: 'boolean',
|
|
153
|
-
description: '
|
|
229
|
+
description: 'Deprecated alias for lsh: "ssdeep" (ignored when lsh is set)',
|
|
154
230
|
default: false
|
|
155
231
|
},
|
|
232
|
+
lsh: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
enum: [...LSH_ALGORITHMS],
|
|
235
|
+
description: 'Compare each permutation\'s web page with the original using a fuzzy hash (slow) to spot cloned phishing pages'
|
|
236
|
+
},
|
|
237
|
+
whois: {
|
|
238
|
+
type: 'boolean',
|
|
239
|
+
description: 'Look up WHOIS creation date and registrar for each domain (slower)',
|
|
240
|
+
default: false
|
|
241
|
+
},
|
|
242
|
+
all_records: {
|
|
243
|
+
type: 'boolean',
|
|
244
|
+
description: 'Return all DNS records instead of only the first of each type',
|
|
245
|
+
default: false
|
|
246
|
+
},
|
|
247
|
+
fuzzers: {
|
|
248
|
+
type: 'array',
|
|
249
|
+
items: { type: 'string', enum: [...FUZZERS] },
|
|
250
|
+
minItems: 1,
|
|
251
|
+
maxItems: FUZZERS.length,
|
|
252
|
+
uniqueItems: true,
|
|
253
|
+
description: 'Only use these fuzzing algorithms (default: all)'
|
|
254
|
+
},
|
|
156
255
|
banners: {
|
|
157
256
|
type: 'boolean',
|
|
158
|
-
description: 'Capture HTTP
|
|
257
|
+
description: 'Capture HTTP and SMTP server banners',
|
|
159
258
|
default: true
|
|
160
259
|
}
|
|
161
260
|
},
|
|
@@ -166,11 +265,11 @@ class DnstwistServer {
|
|
|
166
265
|
}));
|
|
167
266
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
168
267
|
try {
|
|
169
|
-
await this.ensureSetup();
|
|
268
|
+
const runner = await this.ensureSetup();
|
|
170
269
|
if (request.params.name !== 'fuzz_domain') {
|
|
171
270
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
172
271
|
}
|
|
173
|
-
const { domain, nameservers, threads, format, registered_only, mxcheck,
|
|
272
|
+
const { domain, nameservers, threads, format, registered_only, mxcheck, banners, whois, all_records, fuzzers, lsh } = validateFuzzArgs(request.params.arguments);
|
|
174
273
|
// Build command arguments
|
|
175
274
|
const args = [
|
|
176
275
|
domain,
|
|
@@ -184,34 +283,44 @@ class DnstwistServer {
|
|
|
184
283
|
args.push('-t', threads.toString());
|
|
185
284
|
if (mxcheck)
|
|
186
285
|
args.push('--mxcheck');
|
|
187
|
-
if (
|
|
188
|
-
args.push('--
|
|
286
|
+
if (lsh)
|
|
287
|
+
args.push('--lsh', lsh);
|
|
189
288
|
if (banners)
|
|
190
289
|
args.push('-b');
|
|
191
|
-
|
|
192
|
-
|
|
290
|
+
if (whois)
|
|
291
|
+
args.push('-w');
|
|
292
|
+
if (all_records)
|
|
293
|
+
args.push('-a');
|
|
294
|
+
if (fuzzers)
|
|
295
|
+
args.push('--fuzzers', fuzzers.join(','));
|
|
296
|
+
const id = randomUUID();
|
|
297
|
+
const container = runner === 'docker' ? `mcp-dnstwist-${id}` : undefined;
|
|
193
298
|
const stop = new AbortController();
|
|
194
|
-
this.
|
|
299
|
+
this.runningScans.set(id, { stop, container });
|
|
195
300
|
let stdout;
|
|
196
301
|
try {
|
|
197
|
-
|
|
302
|
+
const options = { timeout: TIMEOUT_SECONDS * 1000, signal: AbortSignal.any([extra.signal, stop.signal]) };
|
|
303
|
+
({ stdout } = container
|
|
304
|
+
? await this.execCommand('docker', ['run', '--rm', '--name', container, DOCKER_IMAGE, ...args], options)
|
|
305
|
+
// dnstwist handles SIGTERM itself and may wind down slowly: kill it outright.
|
|
306
|
+
: await this.execCommand('dnstwist', args, { ...options, killSignal: 'SIGKILL', killOnAbort: true }));
|
|
198
307
|
}
|
|
199
308
|
catch (error) {
|
|
200
309
|
const stopped = extra.signal.aborted || stop.signal.aborted;
|
|
201
310
|
const timedOut = Boolean(error.killed) && !stopped;
|
|
202
311
|
// If dnstwist exited on its own, --rm already removed the container.
|
|
203
|
-
if (timedOut || stopped)
|
|
312
|
+
if (container && (timedOut || stopped))
|
|
204
313
|
await this.removeContainer(container);
|
|
205
314
|
if (timedOut) {
|
|
206
315
|
const unit = TIMEOUT_SECONDS === 1 ? 'second' : 'seconds';
|
|
207
316
|
throw new Error(`dnstwist did not finish within ${TIMEOUT_SECONDS} ${unit} and was stopped. ` +
|
|
208
|
-
'Use an IPv4 nameserver (DNS-over-HTTPS is slow), turn off
|
|
317
|
+
'Use an IPv4 nameserver (DNS-over-HTTPS is slow), turn off lsh, whois or banners, ' +
|
|
209
318
|
'or raise DNSTWIST_TIMEOUT_SECONDS.');
|
|
210
319
|
}
|
|
211
320
|
throw error;
|
|
212
321
|
}
|
|
213
322
|
finally {
|
|
214
|
-
this.
|
|
323
|
+
this.runningScans.delete(id);
|
|
215
324
|
}
|
|
216
325
|
// csv and list output are returned as dnstwist printed them.
|
|
217
326
|
if (format !== 'json') {
|
package/build/validate.js
CHANGED
|
@@ -6,6 +6,13 @@ import { domainToASCII } from 'url';
|
|
|
6
6
|
// mistaken for a dnstwist option either.
|
|
7
7
|
const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{1,59})$/i;
|
|
8
8
|
const FORMATS = ['json', 'csv', 'list'];
|
|
9
|
+
// dnstwist's fuzzing algorithms, minus dictionary and tld-swap: those need a
|
|
10
|
+
// word or TLD list file, which this tool doesn't take.
|
|
11
|
+
export const FUZZERS = [
|
|
12
|
+
'addition', 'bitsquatting', 'cyrillic', 'homoglyph', 'hyphenation', 'insertion', 'omission',
|
|
13
|
+
'plural', 'repetition', 'replacement', 'subdomain', 'transposition', 'vowel-swap', 'various',
|
|
14
|
+
];
|
|
15
|
+
export const LSH_ALGORITHMS = ['ssdeep', 'tlsh'];
|
|
9
16
|
const MAX_NAMESERVERS = 10;
|
|
10
17
|
// The DNS-over-HTTPS form dnstwist accepts (dnstwist.py --nameservers parsing).
|
|
11
18
|
const DOH = /^https:\/\/([a-z0-9.-]{4,253})\/dns-query$/;
|
|
@@ -65,6 +72,22 @@ export function validateFuzzArgs(args) {
|
|
|
65
72
|
if (!FORMATS.includes(format)) {
|
|
66
73
|
invalid(`format must be one of ${FORMATS.join(', ')}`);
|
|
67
74
|
}
|
|
75
|
+
let fuzzers;
|
|
76
|
+
if (a.fuzzers !== undefined) {
|
|
77
|
+
const list = a.fuzzers;
|
|
78
|
+
if (!Array.isArray(list) || list.length < 1 || list.length > FUZZERS.length ||
|
|
79
|
+
list.some((f) => typeof f !== 'string' || !FUZZERS.includes(f)) ||
|
|
80
|
+
new Set(list).size !== list.length) {
|
|
81
|
+
invalid(`fuzzers must be 1 to ${FUZZERS.length} unique names from: ${FUZZERS.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
fuzzers = list;
|
|
84
|
+
}
|
|
85
|
+
if (a.lsh !== undefined && !LSH_ALGORITHMS.includes(a.lsh)) {
|
|
86
|
+
invalid(`lsh must be one of ${LSH_ALGORITHMS.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
const ssdeep = optionalBoolean(a, 'ssdeep', false);
|
|
89
|
+
// `ssdeep: true` is the older spelling of `lsh: 'ssdeep'`; an explicit lsh wins.
|
|
90
|
+
const lsh = a.lsh ?? (ssdeep ? 'ssdeep' : undefined);
|
|
68
91
|
return {
|
|
69
92
|
domain: domain,
|
|
70
93
|
nameservers: servers.join(','),
|
|
@@ -72,7 +95,11 @@ export function validateFuzzArgs(args) {
|
|
|
72
95
|
format: format,
|
|
73
96
|
registered_only: optionalBoolean(a, 'registered_only', true),
|
|
74
97
|
mxcheck: optionalBoolean(a, 'mxcheck', true),
|
|
75
|
-
ssdeep
|
|
98
|
+
ssdeep,
|
|
76
99
|
banners: optionalBoolean(a, 'banners', true),
|
|
100
|
+
whois: optionalBoolean(a, 'whois', false),
|
|
101
|
+
all_records: optionalBoolean(a, 'all_records', false),
|
|
102
|
+
...(fuzzers ? { fuzzers } : {}),
|
|
103
|
+
...(lsh ? { lsh } : {}),
|
|
77
104
|
};
|
|
78
105
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burtthecoder/mcp-dnstwist",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "MCP server for dnstwist
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP server for dnstwist: find typosquatting, homoglyph and lookalike domains used for phishing and brand impersonation",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc && chmod +x build/index.js",
|
|
17
|
+
"start": "node build/index.js",
|
|
18
|
+
"dev": "tsc -w",
|
|
17
19
|
"prepublishOnly": "npm run build",
|
|
18
20
|
"test": "npm run build && node --test tests/*.test.mjs"
|
|
19
21
|
},
|
|
@@ -37,11 +39,22 @@
|
|
|
37
39
|
"typosquatting",
|
|
38
40
|
"domain-security",
|
|
39
41
|
"claude",
|
|
40
|
-
"claude-desktop"
|
|
42
|
+
"claude-desktop",
|
|
43
|
+
"mcp-server",
|
|
44
|
+
"model-context-protocol",
|
|
45
|
+
"brand-protection",
|
|
46
|
+
"threat-intelligence",
|
|
47
|
+
"dfir",
|
|
48
|
+
"phishing-detection"
|
|
41
49
|
],
|
|
42
|
-
"author": "
|
|
50
|
+
"author": "w0h1v",
|
|
43
51
|
"license": "MIT",
|
|
44
52
|
"engines": {
|
|
45
53
|
"node": ">=20.3"
|
|
46
|
-
}
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://w0h1v.github.io/mcp-dnstwist/",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/w0h1v/mcp-dnstwist/issues"
|
|
58
|
+
},
|
|
59
|
+
"mcpName": "io.github.w0h1v/dnstwist"
|
|
47
60
|
}
|