@burtthecoder/mcp-dnstwist 1.0.4 → 1.0.6
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 +4 -4
- package/build/index.js +19 -34
- package/build/validate.js +78 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ This tool is designed for legitimate security research purposes. Please:
|
|
|
17
17
|
|
|
18
18
|
## Requirements
|
|
19
19
|
|
|
20
|
-
- Node.js (
|
|
20
|
+
- Node.js (v20 or later)
|
|
21
21
|
- Docker
|
|
22
22
|
- macOS, Linux, or Windows with Docker Desktop installed
|
|
23
23
|
|
|
@@ -38,7 +38,7 @@ npx -y @smithery/cli install @burtthecoder/mcp-dnstwist --client claude
|
|
|
38
38
|
|
|
39
39
|
2. Install the server globally via npm:
|
|
40
40
|
```bash
|
|
41
|
-
npm install -g mcp-dnstwist
|
|
41
|
+
npm install -g @burtthecoder/mcp-dnstwist
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
3. Add to your Claude Desktop configuration file:
|
|
@@ -100,8 +100,8 @@ npm run build
|
|
|
100
100
|
- Description: Generate and analyze domain permutations to detect potential typosquatting, phishing, and brand impersonation
|
|
101
101
|
- Parameters:
|
|
102
102
|
* `domain` (required): Domain name to analyze (e.g., example.com)
|
|
103
|
-
* `nameservers` (optional, default: "1.1.1.1"): Comma-separated
|
|
104
|
-
* `threads` (optional, default: 50): Number of threads for parallel processing
|
|
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
105
|
* `format` (optional, default: "json"): Output format (json, csv, list)
|
|
106
106
|
* `registered_only` (optional, default: true): Show only registered domains
|
|
107
107
|
* `mxcheck` (optional, default: true): Check for MX records
|
package/build/index.js
CHANGED
|
@@ -2,32 +2,16 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import {
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
-
|
|
7
|
+
import { validateFuzzArgs } from './validate.js';
|
|
8
|
+
// execFile runs docker directly with an argument array. No shell is involved,
|
|
9
|
+
// so tool input can never be interpreted as shell syntax (CWE-78, issue #13).
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
8
11
|
const DOCKER_IMAGE = 'elceef/dnstwist';
|
|
9
|
-
function isFuzzDomainArgs(args) {
|
|
10
|
-
if (!args || typeof args !== 'object')
|
|
11
|
-
return false;
|
|
12
|
-
const a = args;
|
|
13
|
-
return typeof a.domain === 'string' &&
|
|
14
|
-
(a.nameservers === undefined || typeof a.nameservers === 'string') &&
|
|
15
|
-
(a.threads === undefined || typeof a.threads === 'number') &&
|
|
16
|
-
(a.format === undefined || ['json', 'csv', 'list'].includes(a.format)) &&
|
|
17
|
-
(a.registered_only === undefined || typeof a.registered_only === 'boolean') &&
|
|
18
|
-
(a.mxcheck === undefined || typeof a.mxcheck === 'boolean') &&
|
|
19
|
-
(a.ssdeep === undefined || typeof a.ssdeep === 'boolean') &&
|
|
20
|
-
(a.banners === undefined || typeof a.banners === 'boolean');
|
|
21
|
-
}
|
|
22
12
|
class DnstwistServer {
|
|
23
13
|
constructor() {
|
|
24
|
-
this.server = new Server({
|
|
25
|
-
name: 'dnstwist-server',
|
|
26
|
-
version: '0.1.0',
|
|
27
|
-
capabilities: {
|
|
28
|
-
tools: {}
|
|
29
|
-
}
|
|
30
|
-
});
|
|
14
|
+
this.server = new Server({ name: 'dnstwist-server', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
31
15
|
this.setupToolHandlers();
|
|
32
16
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
33
17
|
process.on('SIGINT', async () => {
|
|
@@ -40,10 +24,10 @@ class DnstwistServer {
|
|
|
40
24
|
process.exit(1);
|
|
41
25
|
});
|
|
42
26
|
}
|
|
43
|
-
async execCommand(
|
|
44
|
-
console.error('Executing
|
|
27
|
+
async execCommand(file, args) {
|
|
28
|
+
console.error('Executing:', file, JSON.stringify(args));
|
|
45
29
|
try {
|
|
46
|
-
const result = await
|
|
30
|
+
const result = await execFileAsync(file, args, {
|
|
47
31
|
maxBuffer: 10 * 1024 * 1024
|
|
48
32
|
});
|
|
49
33
|
console.error('Command output:', result.stdout);
|
|
@@ -60,19 +44,19 @@ class DnstwistServer {
|
|
|
60
44
|
try {
|
|
61
45
|
console.error('Checking Docker...');
|
|
62
46
|
try {
|
|
63
|
-
await this.execCommand('docker --version');
|
|
47
|
+
await this.execCommand('docker', ['--version']);
|
|
64
48
|
}
|
|
65
49
|
catch (error) {
|
|
66
50
|
throw new Error('Docker is not installed or not running. Please install Docker and try again.');
|
|
67
51
|
}
|
|
68
52
|
console.error('Checking if dnstwist image exists...');
|
|
69
53
|
try {
|
|
70
|
-
await this.execCommand(
|
|
54
|
+
await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
|
|
71
55
|
console.error('Dnstwist image found');
|
|
72
56
|
}
|
|
73
57
|
catch (error) {
|
|
74
58
|
console.error('Dnstwist image not found, pulling...');
|
|
75
|
-
await this.execCommand(
|
|
59
|
+
await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
|
|
76
60
|
console.error('Dnstwist image pulled successfully');
|
|
77
61
|
}
|
|
78
62
|
}
|
|
@@ -96,7 +80,7 @@ class DnstwistServer {
|
|
|
96
80
|
},
|
|
97
81
|
nameservers: {
|
|
98
82
|
type: 'string',
|
|
99
|
-
description: 'Comma-separated
|
|
83
|
+
description: 'Comma-separated DNS servers: IPv4 addresses or DNS-over-HTTPS URLs such as https://cloudflare-dns.com/dns-query (default: 1.1.1.1)',
|
|
100
84
|
default: '1.1.1.1'
|
|
101
85
|
},
|
|
102
86
|
threads: {
|
|
@@ -144,10 +128,7 @@ class DnstwistServer {
|
|
|
144
128
|
if (request.params.name !== 'fuzz_domain') {
|
|
145
129
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
146
130
|
}
|
|
147
|
-
|
|
148
|
-
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for fuzz_domain');
|
|
149
|
-
}
|
|
150
|
-
const { domain, nameservers = '1.1.1.1', threads = 50, format = 'json', registered_only = true, mxcheck = true, ssdeep = false, banners = true } = request.params.arguments;
|
|
131
|
+
const { domain, nameservers, threads, format, registered_only, mxcheck, ssdeep, banners } = validateFuzzArgs(request.params.arguments);
|
|
151
132
|
// Build command arguments
|
|
152
133
|
const args = [
|
|
153
134
|
domain,
|
|
@@ -166,7 +147,11 @@ class DnstwistServer {
|
|
|
166
147
|
if (banners)
|
|
167
148
|
args.push('-b');
|
|
168
149
|
// Run dnstwist in Docker
|
|
169
|
-
const { stdout
|
|
150
|
+
const { stdout } = await this.execCommand('docker', ['run', '--rm', DOCKER_IMAGE, ...args]);
|
|
151
|
+
// csv and list output are returned as dnstwist printed them.
|
|
152
|
+
if (format !== 'json') {
|
|
153
|
+
return { content: [{ type: 'text', text: stdout }] };
|
|
154
|
+
}
|
|
170
155
|
let results;
|
|
171
156
|
try {
|
|
172
157
|
results = JSON.parse(stdout);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { isIP } from 'net';
|
|
3
|
+
import { domainToASCII } from 'url';
|
|
4
|
+
// A hostname: dot-separated labels of letters, digits and inner hyphens, ending
|
|
5
|
+
// in an alphabetic or punycode TLD. It can't start with "-", so it can't be
|
|
6
|
+
// mistaken for a dnstwist option either.
|
|
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
|
+
const FORMATS = ['json', 'csv', 'list'];
|
|
9
|
+
const MAX_NAMESERVERS = 10;
|
|
10
|
+
// The DNS-over-HTTPS form dnstwist accepts (dnstwist.py --nameservers parsing).
|
|
11
|
+
const DOH = /^https:\/\/([a-z0-9.-]{4,253})\/dns-query$/;
|
|
12
|
+
function invalid(message) {
|
|
13
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for fuzz_domain: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
/** Lowercase ASCII (punycode) form of a hostname, or null if it isn't one. */
|
|
16
|
+
function asciiHostname(value) {
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
// domainToASCII silently drops inner tabs and newlines ("a.com\nb" becomes
|
|
19
|
+
// "a.comb"), so reject whitespace and control characters first.
|
|
20
|
+
if (/[\s\x00-\x1f\x7f]/.test(trimmed))
|
|
21
|
+
return null;
|
|
22
|
+
// Handles Unicode (IDN) names; returns '' for invalid input.
|
|
23
|
+
const ascii = domainToASCII(trimmed.replace(/\.$/, ''));
|
|
24
|
+
return HOSTNAME.test(ascii) ? ascii : null;
|
|
25
|
+
}
|
|
26
|
+
function optionalBoolean(a, key, fallback) {
|
|
27
|
+
if (a[key] === undefined)
|
|
28
|
+
return fallback;
|
|
29
|
+
if (typeof a[key] !== 'boolean')
|
|
30
|
+
invalid(`${key} must be a boolean`);
|
|
31
|
+
return a[key];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate and normalise fuzz_domain arguments. Everything returned is safe to
|
|
35
|
+
* pass to dnstwist as a separate argv entry: no value can start with "-".
|
|
36
|
+
*/
|
|
37
|
+
export function validateFuzzArgs(args) {
|
|
38
|
+
if (!args || typeof args !== 'object')
|
|
39
|
+
invalid('arguments must be an object');
|
|
40
|
+
const a = args;
|
|
41
|
+
const domain = typeof a.domain === 'string' ? asciiHostname(a.domain) : null;
|
|
42
|
+
if (!domain)
|
|
43
|
+
invalid('domain must be a domain name such as example.com');
|
|
44
|
+
const rawNameservers = a.nameservers === undefined ? '1.1.1.1' : a.nameservers;
|
|
45
|
+
if (typeof rawNameservers !== 'string')
|
|
46
|
+
invalid('nameservers must be a string');
|
|
47
|
+
// dnstwist only accepts IPv4 addresses and https://<host>/dns-query URLs, so
|
|
48
|
+
// anything else would fail inside the container anyway.
|
|
49
|
+
const servers = rawNameservers.split(',').map((ns) => {
|
|
50
|
+
const trimmed = ns.trim();
|
|
51
|
+
if (isIP(trimmed) === 4)
|
|
52
|
+
return trimmed;
|
|
53
|
+
const doh = trimmed.match(DOH);
|
|
54
|
+
return doh && (isIP(doh[1]) === 4 || HOSTNAME.test(doh[1])) ? trimmed : null;
|
|
55
|
+
});
|
|
56
|
+
if (servers.length > MAX_NAMESERVERS || servers.some((ns) => !ns)) {
|
|
57
|
+
invalid(`nameservers must be up to ${MAX_NAMESERVERS} comma-separated IPv4 addresses ` +
|
|
58
|
+
'or DNS-over-HTTPS URLs like https://cloudflare-dns.com/dns-query');
|
|
59
|
+
}
|
|
60
|
+
const threads = a.threads === undefined ? 50 : a.threads;
|
|
61
|
+
if (typeof threads !== 'number' || !Number.isInteger(threads) || threads < 1 || threads > 100) {
|
|
62
|
+
invalid('threads must be an integer from 1 to 100');
|
|
63
|
+
}
|
|
64
|
+
const format = a.format === undefined ? 'json' : a.format;
|
|
65
|
+
if (!FORMATS.includes(format)) {
|
|
66
|
+
invalid(`format must be one of ${FORMATS.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
domain: domain,
|
|
70
|
+
nameservers: servers.join(','),
|
|
71
|
+
threads: threads,
|
|
72
|
+
format: format,
|
|
73
|
+
registered_only: optionalBoolean(a, 'registered_only', true),
|
|
74
|
+
mxcheck: optionalBoolean(a, 'mxcheck', true),
|
|
75
|
+
ssdeep: optionalBoolean(a, 'ssdeep', false),
|
|
76
|
+
banners: optionalBoolean(a, 'banners', true),
|
|
77
|
+
};
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burtthecoder/mcp-dnstwist",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "MCP server for dnstwist - DNS fuzzing to detect typosquatting, phishing and corporate espionage",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,18 +14,19 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc && chmod +x build/index.js",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"test": "npm run build && node --test tests/*.test.mjs"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@modelcontextprotocol/sdk": "^
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.32.1"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^20.
|
|
24
|
-
"typescript": "^5.
|
|
24
|
+
"@types/node": "^20.19.43",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
25
26
|
},
|
|
26
27
|
"repository": {
|
|
27
28
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/
|
|
29
|
+
"url": "git+https://github.com/w0h1v/mcp-dnstwist.git"
|
|
29
30
|
},
|
|
30
31
|
"keywords": [
|
|
31
32
|
"mcp",
|
|
@@ -41,6 +42,6 @@
|
|
|
41
42
|
"author": "burtmacklin",
|
|
42
43
|
"license": "MIT",
|
|
43
44
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
45
|
+
"node": ">=20"
|
|
45
46
|
}
|
|
46
47
|
}
|