# Directory Structure
```
├── .gitignore
├── index.js
├── LICENSE
├── package.json
├── README.md
└── Wire-MCP.png
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log
4 | yarn-debug.log
5 | yarn-error.log
6 | package-lock.json
7 | yarn.lock
8 | .pcap
9 | *.cap
10 |
11 | # Environment variables
12 | .env
13 | .env.local
14 | .env.*.local
15 |
16 | # PCAP files (usually large and contain sensitive data)
17 | *.pcap
18 | *.pcapng
19 | captures/
20 |
21 | # Operating System
22 | .DS_Store
23 | .DS_Store?
24 | ._*
25 | .Spotlight-V100
26 | .Trashes
27 | ehthumbs.db
28 | Thumbs.db
29 |
30 | # IDE and Editor files
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | *~
36 |
37 | # Logs
38 | logs/
39 | *.log
40 | npm-debug.log*
41 | yarn-debug.log*
42 | yarn-error.log*
43 |
44 | # Build output
45 | dist/
46 | build/
47 | out/
48 |
49 | # Test coverage
50 | coverage/
51 |
52 | # Temporary files
53 | tmp/
54 | temp/
55 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | 
2 |
3 |
4 | # WireMCP
5 | WireMCP is a Model Context Protocol (MCP) server designed to empower Large Language Models (LLMs) with real-time network traffic analysis capabilities. By leveraging tools built on top of Wireshark's `tshark`, WireMCP captures and processes live network data, providing LLMs with structured context to assist in tasks like threat hunting, network diagnostics, and anomaly detection.
6 |
7 | # Features
8 | WireMCP exposes the following tools to MCP clients, enhancing LLM understanding of network activity:
9 |
10 | - **`capture_packets`**: Captures live traffic and returns raw packet data as JSON, enabling LLMs to analyze packet-level details (e.g., IP addresses, ports, HTTP methods).
11 | - **`get_summary_stats`**: Provides protocol hierarchy statistics, giving LLMs an overview of traffic composition (e.g., TCP vs. UDP usage).
12 | - **`get_conversations`**: Delivers TCP/UDP conversation statistics, allowing LLMs to track communication flows between endpoints.
13 | - **`check_threats`**: Captures IPs and checks them against the URLhaus blacklist, equipping LLMs with threat intelligence context for identifying malicious activity.
14 | - **`check_ip_threats`**: Performs targeted threat intelligence lookups for specific IP addresses against multiple threat feeds, providing detailed reputation and threat data.
15 | - **`analyze_pcap`**: Analyzes PCAP files to provide comprehensive packet data in JSON format, enabling detailed post-capture analysis of network traffic.
16 | - **`extract_credentials`**: Scans PCAP files for potential credentials from various protocols (HTTP Basic Auth, FTP, Telnet), aiding in security audits and forensic analysis.
17 |
18 |
19 | ## How It Helps LLMs
20 | WireMCP bridges the gap between raw network data and LLM comprehension by:
21 | - **Contextualizing Traffic**: Converts live packet captures into structured outputs (JSON, stats) that LLMs can parse and reason about.
22 | - **Threat Detection**: Integrates IOCs (currently URLhaus) to flag suspicious IPs, enhancing LLM-driven security analysis.
23 | - **Diagnostics**: Offers detailed traffic insights, enabling LLMs to assist with troubleshooting or identifying anomalies.
24 | - **Narrative Generation**: LLM's can Transform complex packet captures into coherent stories, making network analysis accessible to non-technical users.
25 |
26 | # Installation
27 |
28 | ## Prerequisites
29 | - Mac / Windows / Linux
30 | - [Wireshark](https://www.wireshark.org/download.html) (with `tshark` installed and accessible in PATH)
31 | - Node.js (v16+ recommended)
32 | - npm (for dependency installation)
33 |
34 | ## Setup
35 | 1. Clone the repository:
36 | ```bash
37 | git clone https://github.com/0xkoda/WireMCP.git
38 | cd WireMCP
39 | ```
40 |
41 | 2. Install dependencies:
42 | ```bash
43 | npm install
44 | ```
45 |
46 | 3. Run the MCP server:
47 | ```bash
48 | node index.js
49 | ```
50 |
51 | > **Note**: Ensure `tshark` is in your PATH. WireMCP will auto-detect it or fall back to common install locations (e.g., `/Applications/Wireshark.app/Contents/MacOS/tshark` on macOS).
52 |
53 | # Usage with MCP Clients
54 |
55 | WireMCP works with any MCP-compliant client. Below are examples for popular clients:
56 |
57 | ## Example 1: Cursor
58 |
59 | Edit `mcp.json` in Cursor -> Settings -> MCP :
60 |
61 | ```json
62 | {
63 | "mcpServers": {
64 | "wiremcp": {
65 | "command": "node",
66 | "args": [
67 | "/ABSOLUTE_PATH_TO/WireMCP/index.js"
68 | ]
69 | }
70 | }
71 | }
72 | ```
73 |
74 | **Location (macOS)**: `/Users/YOUR_USER/Library/Application Support/Claude/claude_desktop_config.json`
75 |
76 | ## Other Clients
77 |
78 | This MCP will work well with any client. Use the command `node /path/to/WireMCP/index.js` in their MCP server settings.
79 |
80 | # Example Output
81 |
82 | Running `check_threats` might yield:
83 |
84 | ```
85 | Captured IPs:
86 | 174.67.0.227
87 | 52.196.136.253
88 |
89 | Threat check against URLhaus blacklist:
90 | No threats detected in URLhaus blacklist.
91 | ```
92 |
93 | Running `analyze_pcap` on a capture file:
94 |
95 | ```json
96 | {
97 | "content": [{
98 | "type": "text",
99 | "text": "Analyzed PCAP: ./capture.pcap\n\nUnique IPs:\n192.168.0.2\n192.168.0.1\n\nProtocols:\neth:ethertype:ip:tcp\neth:ethertype:ip:tcp:telnet\n\nPacket Data:\n[{\"layers\":{\"frame.number\":[\"1\"],\"ip.src\":[\"192.168.0.2\"],\"ip.dst\":[\"192.168.0.1\"],\"tcp.srcport\":[\"1550\"],\"tcp.dstport\":[\"23\"]}}]"
100 | }]
101 | }
102 | ```
103 |
104 |
105 | LLMs can use these outputs to:
106 | - Provide natural language explanations of network activity
107 | - Identify patterns and potential security concerns
108 | - Offer context-aware recommendations
109 | - Generate human-readable reports
110 |
111 | # Roadmap
112 |
113 | - **Expand IOC Providers**: Currently uses URLhaus for threat checks. Future updates will integrate additional sources (e.g., IPsum, Emerging Threats) for broader coverage.
114 |
115 |
116 | # Contributing
117 |
118 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
119 |
120 | # License
121 |
122 | [MIT](LICENSE)
123 |
124 | # Acknowledgments
125 |
126 | - Wireshark/tshark team for their excellent packet analysis tools
127 | - Model Context Protocol community for the framework and specifications
128 | - URLhaus for providing threat intelligence data
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "wiremcp",
3 | "version": "1.0.0",
4 | "description": "An MCP for network sleuthing",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "0xKoda",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@modelcontextprotocol/sdk": "^1.8.0",
13 | "axios": "^1.8.4",
14 | "child_process": "^1.0.2",
15 | "util": "^0.12.5",
16 | "which": "^5.0.0",
17 | "zod": "^3.24.2"
18 | }
19 | }
20 |
```
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
```javascript
1 | // index.js - WireMCP Server
2 | const axios = require('axios');
3 | const { exec } = require('child_process');
4 | const { promisify } = require('util');
5 | const which = require('which');
6 | const fs = require('fs').promises;
7 | const execAsync = promisify(exec);
8 | const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
9 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
10 | const { z } = require('zod');
11 |
12 | // Redirect console.log to stderr
13 | const originalConsoleLog = console.log;
14 | console.log = (...args) => console.error(...args);
15 |
16 | // Dynamically locate tshark
17 | async function findTshark() {
18 | try {
19 | const tsharkPath = await which('tshark');
20 | console.error(`Found tshark at: ${tsharkPath}`);
21 | return tsharkPath;
22 | } catch (err) {
23 | console.error('which failed to find tshark:', err.message);
24 | const fallbacks = process.platform === 'win32'
25 | ? ['C:\\Program Files\\Wireshark\\tshark.exe', 'C:\\Program Files (x86)\\Wireshark\\tshark.exe']
26 | : ['/usr/bin/tshark', '/usr/local/bin/tshark', '/opt/homebrew/bin/tshark', '/Applications/Wireshark.app/Contents/MacOS/tshark'];
27 |
28 | for (const path of fallbacks) {
29 | try {
30 | await execAsync(`${path} -v`);
31 | console.error(`Found tshark at fallback: ${path}`);
32 | return path;
33 | } catch (e) {
34 | console.error(`Fallback ${path} failed: ${e.message}`);
35 | }
36 | }
37 | throw new Error('tshark not found. Please install Wireshark (https://www.wireshark.org/download.html) and ensure tshark is in your PATH.');
38 | }
39 | }
40 |
41 | // Initialize MCP server
42 | const server = new McpServer({
43 | name: 'wiremcp',
44 | version: '1.0.0',
45 | });
46 |
47 | // Tool 1: Capture live packet data
48 | server.tool(
49 | 'capture_packets',
50 | 'Capture live traffic and provide raw packet data as JSON for LLM analysis',
51 | {
52 | interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
53 | duration: z.number().optional().default(5).describe('Capture duration in seconds'),
54 | },
55 | async (args) => {
56 | try {
57 | const tsharkPath = await findTshark();
58 | const { interface, duration } = args;
59 | const tempPcap = 'temp_capture.pcap';
60 | console.error(`Capturing packets on ${interface} for ${duration}s`);
61 |
62 | await execAsync(
63 | `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
64 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
65 | );
66 |
67 | const { stdout, stderr } = await execAsync(
68 | `${tsharkPath} -r "${tempPcap}" -T json -e frame.number -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.flags -e frame.time -e http.request.method -e http.response.code`,
69 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
70 | );
71 | if (stderr) console.error(`tshark stderr: ${stderr}`);
72 | let packets = JSON.parse(stdout);
73 |
74 | const maxChars = 720000;
75 | let jsonString = JSON.stringify(packets);
76 | if (jsonString.length > maxChars) {
77 | const trimFactor = maxChars / jsonString.length;
78 | const trimCount = Math.floor(packets.length * trimFactor);
79 | packets = packets.slice(0, trimCount);
80 | jsonString = JSON.stringify(packets);
81 | console.error(`Trimmed packets from ${packets.length} to ${trimCount} to fit ${maxChars} chars`);
82 | }
83 |
84 | await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`));
85 |
86 | return {
87 | content: [{
88 | type: 'text',
89 | text: `Captured packet data (JSON for LLM analysis):\n${jsonString}`,
90 | }],
91 | };
92 | } catch (error) {
93 | console.error(`Error in capture_packets: ${error.message}`);
94 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
95 | }
96 | }
97 | );
98 |
99 | // Tool 2: Capture and provide summary statistics
100 | server.tool(
101 | 'get_summary_stats',
102 | 'Capture live traffic and provide protocol hierarchy statistics for LLM analysis',
103 | {
104 | interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
105 | duration: z.number().optional().default(5).describe('Capture duration in seconds'),
106 | },
107 | async (args) => {
108 | try {
109 | const tsharkPath = await findTshark();
110 | const { interface, duration } = args;
111 | const tempPcap = 'temp_capture.pcap';
112 | console.error(`Capturing summary stats on ${interface} for ${duration}s`);
113 |
114 | await execAsync(
115 | `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
116 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
117 | );
118 |
119 | const { stdout, stderr } = await execAsync(
120 | `${tsharkPath} -r "${tempPcap}" -qz io,phs`,
121 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
122 | );
123 | if (stderr) console.error(`tshark stderr: ${stderr}`);
124 |
125 | await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`));
126 |
127 | return {
128 | content: [{
129 | type: 'text',
130 | text: `Protocol hierarchy statistics for LLM analysis:\n${stdout}`,
131 | }],
132 | };
133 | } catch (error) {
134 | console.error(`Error in get_summary_stats: ${error.message}`);
135 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
136 | }
137 | }
138 | );
139 |
140 | // Tool 3: Capture and provide conversation stats
141 | server.tool(
142 | 'get_conversations',
143 | 'Capture live traffic and provide TCP/UDP conversation statistics for LLM analysis',
144 | {
145 | interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
146 | duration: z.number().optional().default(5).describe('Capture duration in seconds'),
147 | },
148 | async (args) => {
149 | try {
150 | const tsharkPath = await findTshark();
151 | const { interface, duration } = args;
152 | const tempPcap = 'temp_capture.pcap';
153 | console.error(`Capturing conversations on ${interface} for ${duration}s`);
154 |
155 | await execAsync(
156 | `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
157 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
158 | );
159 |
160 | const { stdout, stderr } = await execAsync(
161 | `${tsharkPath} -r "${tempPcap}" -qz conv,tcp`,
162 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
163 | );
164 | if (stderr) console.error(`tshark stderr: ${stderr}`);
165 |
166 | await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`));
167 |
168 | return {
169 | content: [{
170 | type: 'text',
171 | text: `TCP/UDP conversation statistics for LLM analysis:\n${stdout}`,
172 | }],
173 | };
174 | } catch (error) {
175 | console.error(`Error in get_conversations: ${error.message}`);
176 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
177 | }
178 | }
179 | );
180 |
181 | // Tool 4: Capture traffic and check threats against URLhaus
182 | server.tool(
183 | 'check_threats',
184 | 'Capture live traffic and check IPs against URLhaus blacklist',
185 | {
186 | interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
187 | duration: z.number().optional().default(5).describe('Capture duration in seconds'),
188 | },
189 | async (args) => {
190 | try {
191 | const tsharkPath = await findTshark();
192 | const { interface, duration } = args;
193 | const tempPcap = 'temp_capture.pcap';
194 | console.error(`Capturing traffic on ${interface} for ${duration}s to check threats`);
195 |
196 | await execAsync(
197 | `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
198 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
199 | );
200 |
201 | const { stdout } = await execAsync(
202 | `${tsharkPath} -r "${tempPcap}" -T fields -e ip.src -e ip.dst`,
203 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
204 | );
205 | const ips = [...new Set(stdout.split('\n').flatMap(line => line.split('\t')).filter(ip => ip && ip !== 'unknown'))];
206 | console.error(`Captured ${ips.length} unique IPs: ${ips.join(', ')}`);
207 |
208 | const urlhausUrl = 'https://urlhaus.abuse.ch/downloads/text/';
209 | console.error(`Fetching URLhaus blacklist from ${urlhausUrl}`);
210 | let urlhausData;
211 | let urlhausThreats = [];
212 | try {
213 | const response = await axios.get(urlhausUrl);
214 | console.error(`URLhaus response status: ${response.status}, length: ${response.data.length} chars`);
215 | console.error(`URLhaus raw data (first 200 chars): ${response.data.slice(0, 200)}`);
216 | const ipRegex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
217 | urlhausData = [...new Set(response.data.split('\n')
218 | .map(line => {
219 | const match = line.match(ipRegex);
220 | return match ? match[0] : null;
221 | })
222 | .filter(ip => ip))];
223 | console.error(`URLhaus lookup successful: ${urlhausData.length} blacklist IPs fetched`);
224 | console.error(`Sample URLhaus IPs: ${urlhausData.slice(0, 5).join(', ') || 'None'}`);
225 | urlhausThreats = ips.filter(ip => urlhausData.includes(ip));
226 | console.error(`Checked IPs against URLhaus: ${urlhausThreats.length} threats found - ${urlhausThreats.join(', ') || 'None'}`);
227 | } catch (e) {
228 | console.error(`Failed to fetch URLhaus data: ${e.message}`);
229 | urlhausData = [];
230 | }
231 |
232 | const outputText = `Captured IPs:\n${ips.join('\n')}\n\n` +
233 | `Threat check against URLhaus blacklist:\n${
234 | urlhausThreats.length > 0 ? `Potential threats: ${urlhausThreats.join(', ')}` : 'No threats detected in URLhaus blacklist.'
235 | }`;
236 |
237 | await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`));
238 |
239 | return {
240 | content: [{ type: 'text', text: outputText }],
241 | };
242 | } catch (error) {
243 | console.error(`Error in check_threats: ${error.message}`);
244 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
245 | }
246 | }
247 | );
248 |
249 | // Tool 5: Check a specific IP against URLhaus IOCs
250 | server.tool(
251 | 'check_ip_threats',
252 | 'Check a given IP address against URLhaus blacklist for IOCs',
253 | {
254 | ip: z.string().regex(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/).describe('IP address to check (e.g., 192.168.1.1)'),
255 | },
256 | async (args) => {
257 | try {
258 | const { ip } = args;
259 | console.error(`Checking IP ${ip} against URLhaus blacklist`);
260 |
261 | const urlhausUrl = 'https://urlhaus.abuse.ch/downloads/text/';
262 | console.error(`Fetching URLhaus blacklist from ${urlhausUrl}`);
263 | let urlhausData;
264 | let isThreat = false;
265 | try {
266 | const response = await axios.get(urlhausUrl);
267 | console.error(`URLhaus response status: ${response.status}, length: ${response.data.length} chars`);
268 | console.error(`URLhaus raw data (first 200 chars): ${response.data.slice(0, 200)}`);
269 | const ipRegex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
270 | urlhausData = [...new Set(response.data.split('\n')
271 | .map(line => {
272 | const match = line.match(ipRegex);
273 | return match ? match[0] : null;
274 | })
275 | .filter(ip => ip))];
276 | console.error(`URLhaus lookup successful: ${urlhausData.length} blacklist IPs fetched`);
277 | console.error(`Sample URLhaus IPs: ${urlhausData.slice(0, 5).join(', ') || 'None'}`);
278 | isThreat = urlhausData.includes(ip);
279 | console.error(`IP ${ip} checked against URLhaus: ${isThreat ? 'Threat found' : 'No threat found'}`);
280 | } catch (e) {
281 | console.error(`Failed to fetch URLhaus data: ${e.message}`);
282 | urlhausData = [];
283 | }
284 |
285 | const outputText = `IP checked: ${ip}\n\n` +
286 | `Threat check against URLhaus blacklist:\n${
287 | isThreat ? 'Potential threat detected in URLhaus blacklist.' : 'No threat detected in URLhaus blacklist.'
288 | }`;
289 |
290 | return {
291 | content: [{ type: 'text', text: outputText }],
292 | };
293 | } catch (error) {
294 | console.error(`Error in check_ip_threats: ${error.message}`);
295 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
296 | }
297 | }
298 | );
299 |
300 | // Tool 6: Analyze an existing PCAP file for general context
301 | server.tool(
302 | 'analyze_pcap',
303 | 'Analyze a PCAP file and provide general packet data as JSON for LLM analysis',
304 | {
305 | pcapPath: z.string().describe('Path to the PCAP file to analyze (e.g., ./demo.pcap)'),
306 | },
307 | async (args) => {
308 | try {
309 | const tsharkPath = await findTshark();
310 | const { pcapPath } = args;
311 | console.error(`Analyzing PCAP file: ${pcapPath}`);
312 |
313 | // Check if file exists
314 | await fs.access(pcapPath);
315 |
316 | // Extract broad packet data
317 | const { stdout, stderr } = await execAsync(
318 | `${tsharkPath} -r "${pcapPath}" -T json -e frame.number -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e udp.srcport -e udp.dstport -e http.host -e http.request.uri -e frame.protocols`,
319 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
320 | );
321 | if (stderr) console.error(`tshark stderr: ${stderr}`);
322 | const packets = JSON.parse(stdout);
323 |
324 | const ips = [...new Set(packets.flatMap(p => [
325 | p._source?.layers['ip.src']?.[0],
326 | p._source?.layers['ip.dst']?.[0]
327 | ]).filter(ip => ip))];
328 | console.error(`Found ${ips.length} unique IPs: ${ips.join(', ')}`);
329 |
330 | const urls = packets
331 | .filter(p => p._source?.layers['http.host'] && p._source?.layers['http.request.uri'])
332 | .map(p => `http://${p._source.layers['http.host'][0]}${p._source.layers['http.request.uri'][0]}`);
333 | console.error(`Found ${urls.length} URLs: ${urls.join(', ') || 'None'}`);
334 |
335 | const protocols = [...new Set(packets.map(p => p._source?.layers['frame.protocols']?.[0]))].filter(p => p);
336 | console.error(`Found protocols: ${protocols.join(', ') || 'None'}`);
337 |
338 | const maxChars = 720000;
339 | let jsonString = JSON.stringify(packets);
340 | if (jsonString.length > maxChars) {
341 | const trimFactor = maxChars / jsonString.length;
342 | const trimCount = Math.floor(packets.length * trimFactor);
343 | packets.splice(trimCount);
344 | jsonString = JSON.stringify(packets);
345 | console.error(`Trimmed packets from ${packets.length} to ${trimCount} to fit ${maxChars} chars`);
346 | }
347 |
348 | const outputText = `Analyzed PCAP: ${pcapPath}\n\n` +
349 | `Unique IPs:\n${ips.join('\n')}\n\n` +
350 | `URLs:\n${urls.length > 0 ? urls.join('\n') : 'None'}\n\n` +
351 | `Protocols:\n${protocols.join('\n') || 'None'}\n\n` +
352 | `Packet Data (JSON for LLM):\n${jsonString}`;
353 |
354 | return {
355 | content: [{ type: 'text', text: outputText }],
356 | };
357 | } catch (error) {
358 | console.error(`Error in analyze_pcap: ${error.message}`);
359 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
360 | }
361 | }
362 | );
363 |
364 | // Tool 7: Extract credentials from a PCAP file
365 | server.tool(
366 | 'extract_credentials',
367 | 'Extract potential credentials (HTTP Basic Auth, FTP, Telnet) from a PCAP file for LLM analysis',
368 | {
369 | pcapPath: z.string().describe('Path to the PCAP file to analyze (e.g., ./demo.pcap)'),
370 | },
371 | async (args) => {
372 | try {
373 | const tsharkPath = await findTshark();
374 | const { pcapPath } = args;
375 | console.error(`Extracting credentials from PCAP file: ${pcapPath}`);
376 |
377 | await fs.access(pcapPath);
378 |
379 | // Extract plaintext credentials
380 | const { stdout: plaintextOut } = await execAsync(
381 | `${tsharkPath} -r "${pcapPath}" -T fields -e http.authbasic -e ftp.request.command -e ftp.request.arg -e telnet.data -e frame.number`,
382 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
383 | );
384 |
385 | // Extract Kerberos credentials
386 | const { stdout: kerberosOut } = await execAsync(
387 | `${tsharkPath} -r "${pcapPath}" -T fields -e kerberos.CNameString -e kerberos.realm -e kerberos.cipher -e kerberos.type -e kerberos.msg_type -e frame.number`,
388 | { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
389 | );
390 |
391 | const lines = plaintextOut.split('\n').filter(line => line.trim());
392 | const packets = lines.map(line => {
393 | const [authBasic, ftpCmd, ftpArg, telnetData, frameNumber] = line.split('\t');
394 | return {
395 | authBasic: authBasic || '',
396 | ftpCmd: ftpCmd || '',
397 | ftpArg: ftpArg || '',
398 | telnetData: telnetData || '',
399 | frameNumber: frameNumber || ''
400 | };
401 | });
402 |
403 | const credentials = {
404 | plaintext: [],
405 | encrypted: []
406 | };
407 |
408 | // Process HTTP Basic Auth
409 | packets.forEach(p => {
410 | if (p.authBasic) {
411 | const [username, password] = Buffer.from(p.authBasic, 'base64').toString().split(':');
412 | credentials.plaintext.push({ type: 'HTTP Basic Auth', username, password, frame: p.frameNumber });
413 | }
414 | });
415 |
416 | // Process FTP
417 | packets.forEach(p => {
418 | if (p.ftpCmd === 'USER') {
419 | credentials.plaintext.push({ type: 'FTP', username: p.ftpArg, password: '', frame: p.frameNumber });
420 | }
421 | if (p.ftpCmd === 'PASS') {
422 | const lastUser = credentials.plaintext.findLast(c => c.type === 'FTP' && !c.password);
423 | if (lastUser) lastUser.password = p.ftpArg;
424 | }
425 | });
426 |
427 | // Process Telnet
428 | packets.forEach(p => {
429 | if (p.telnetData) {
430 | const telnetStr = p.telnetData.trim();
431 | if (telnetStr.toLowerCase().includes('login:') || telnetStr.toLowerCase().includes('password:')) {
432 | credentials.plaintext.push({ type: 'Telnet Prompt', data: telnetStr, frame: p.frameNumber });
433 | } else if (telnetStr && !telnetStr.match(/[A-Z][a-z]+:/) && !telnetStr.includes(' ')) {
434 | const lastPrompt = credentials.plaintext.findLast(c => c.type === 'Telnet Prompt');
435 | if (lastPrompt && lastPrompt.data.toLowerCase().includes('login:')) {
436 | credentials.plaintext.push({ type: 'Telnet', username: telnetStr, password: '', frame: p.frameNumber });
437 | } else if (lastPrompt && lastPrompt.data.toLowerCase().includes('password:')) {
438 | const lastUser = credentials.plaintext.findLast(c => c.type === 'Telnet' && !c.password);
439 | if (lastUser) lastUser.password = telnetStr;
440 | else credentials.plaintext.push({ type: 'Telnet', username: '', password: telnetStr, frame: p.frameNumber });
441 | }
442 | }
443 | }
444 | });
445 |
446 | // Process Kerberos credentials
447 | const kerberosLines = kerberosOut.split('\n').filter(line => line.trim());
448 | kerberosLines.forEach(line => {
449 | const [cname, realm, cipher, type, msgType, frameNumber] = line.split('\t');
450 |
451 | if (cipher && type) {
452 | let hashFormat = '';
453 | // Format hash based on message type
454 | if (msgType === '10' || msgType === '30') { // AS-REQ or TGS-REQ
455 | hashFormat = '$krb5pa$23$';
456 | if (cname) hashFormat += `${cname}$`;
457 | if (realm) hashFormat += `${realm}$`;
458 | hashFormat += cipher;
459 | } else if (msgType === '11') { // AS-REP
460 | hashFormat = '$krb5asrep$23$';
461 | if (cname) hashFormat += `${cname}@`;
462 | if (realm) hashFormat += `${realm}$`;
463 | hashFormat += cipher;
464 | }
465 |
466 | if (hashFormat) {
467 | credentials.encrypted.push({
468 | type: 'Kerberos',
469 | hash: hashFormat,
470 | username: cname || 'unknown',
471 | realm: realm || 'unknown',
472 | frame: frameNumber,
473 | crackingMode: msgType === '11' ? 'hashcat -m 18200' : 'hashcat -m 7500'
474 | });
475 | }
476 | }
477 | });
478 |
479 | console.error(`Found ${credentials.plaintext.length} plaintext and ${credentials.encrypted.length} encrypted credentials`);
480 |
481 | const outputText = `Analyzed PCAP: ${pcapPath}\n\n` +
482 | `Plaintext Credentials:\n${credentials.plaintext.length > 0 ?
483 | credentials.plaintext.map(c =>
484 | c.type === 'Telnet Prompt' ?
485 | `${c.type}: ${c.data} (Frame ${c.frame})` :
486 | `${c.type}: ${c.username}:${c.password} (Frame ${c.frame})`
487 | ).join('\n') :
488 | 'None'}\n\n` +
489 | `Encrypted/Hashed Credentials:\n${credentials.encrypted.length > 0 ?
490 | credentials.encrypted.map(c =>
491 | `${c.type}: User=${c.username} Realm=${c.realm} (Frame ${c.frame})\n` +
492 | `Hash=${c.hash}\n` +
493 | `Cracking Command: ${c.crackingMode}\n`
494 | ).join('\n') :
495 | 'None'}\n\n` +
496 | `Note: Encrypted credentials can be cracked using tools like John the Ripper or hashcat.\n` +
497 | `For Kerberos hashes:\n` +
498 | `- AS-REQ/TGS-REQ: hashcat -m 7500 or john --format=krb5pa-md5\n` +
499 | `- AS-REP: hashcat -m 18200 or john --format=krb5asrep`;
500 |
501 | return {
502 | content: [{ type: 'text', text: outputText }],
503 | };
504 | } catch (error) {
505 | console.error(`Error in extract_credentials: ${error.message}`);
506 | return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
507 | }
508 | }
509 | );
510 |
511 | // Add prompts for each tool
512 | server.prompt(
513 | 'capture_packets_prompt',
514 | {
515 | interface: z.string().optional().describe('Network interface to capture from'),
516 | duration: z.number().optional().describe('Duration in seconds to capture'),
517 | },
518 | ({ interface = 'en0', duration = 5 }) => ({
519 | messages: [{
520 | role: 'user',
521 | content: {
522 | type: 'text',
523 | text: `Please analyze the network traffic on interface ${interface} for ${duration} seconds and provide insights about:
524 | 1. The types of traffic observed
525 | 2. Any notable patterns or anomalies
526 | 3. Key IP addresses and ports involved
527 | 4. Potential security concerns`
528 | }
529 | }]
530 | })
531 | );
532 |
533 | server.prompt(
534 | 'summary_stats_prompt',
535 | {
536 | interface: z.string().optional().describe('Network interface to capture from'),
537 | duration: z.number().optional().describe('Duration in seconds to capture'),
538 | },
539 | ({ interface = 'en0', duration = 5 }) => ({
540 | messages: [{
541 | role: 'user',
542 | content: {
543 | type: 'text',
544 | text: `Please provide a summary of network traffic statistics from interface ${interface} over ${duration} seconds, focusing on:
545 | 1. Protocol distribution
546 | 2. Traffic volume by protocol
547 | 3. Notable patterns in protocol usage
548 | 4. Potential network health indicators`
549 | }
550 | }]
551 | })
552 | );
553 |
554 | server.prompt(
555 | 'conversations_prompt',
556 | {
557 | interface: z.string().optional().describe('Network interface to capture from'),
558 | duration: z.number().optional().describe('Duration in seconds to capture'),
559 | },
560 | ({ interface = 'en0', duration = 5 }) => ({
561 | messages: [{
562 | role: 'user',
563 | content: {
564 | type: 'text',
565 | text: `Please analyze network conversations on interface ${interface} for ${duration} seconds and identify:
566 | 1. Most active IP pairs
567 | 2. Conversation durations and data volumes
568 | 3. Unusual communication patterns
569 | 4. Potential indicators of network issues`
570 | }
571 | }]
572 | })
573 | );
574 |
575 | server.prompt(
576 | 'check_threats_prompt',
577 | {
578 | interface: z.string().optional().describe('Network interface to capture from'),
579 | duration: z.number().optional().describe('Duration in seconds to capture'),
580 | },
581 | ({ interface = 'en0', duration = 5 }) => ({
582 | messages: [{
583 | role: 'user',
584 | content: {
585 | type: 'text',
586 | text: `Please analyze traffic on interface ${interface} for ${duration} seconds and check for security threats:
587 | 1. Compare captured IPs against URLhaus blacklist
588 | 2. Identify potential malicious activity
589 | 3. Highlight any concerning patterns
590 | 4. Provide security recommendations`
591 | }
592 | }]
593 | })
594 | );
595 |
596 | server.prompt(
597 | 'check_ip_threats_prompt',
598 | {
599 | ip: z.string().describe('IP address to check'),
600 | },
601 | ({ ip }) => ({
602 | messages: [{
603 | role: 'user',
604 | content: {
605 | type: 'text',
606 | text: `Please analyze the following IP address (${ip}) for potential security threats:
607 | 1. Check against URLhaus blacklist
608 | 2. Evaluate the IP's reputation
609 | 3. Identify any known malicious activity
610 | 4. Provide security recommendations`
611 | }
612 | }]
613 | })
614 | );
615 |
616 | server.prompt(
617 | 'analyze_pcap_prompt',
618 | {
619 | pcapPath: z.string().describe('Path to the PCAP file'),
620 | },
621 | ({ pcapPath }) => ({
622 | messages: [{
623 | role: 'user',
624 | content: {
625 | type: 'text',
626 | text: `Please analyze the PCAP file at ${pcapPath} and provide insights about:
627 | 1. Overall traffic patterns
628 | 2. Unique IPs and their interactions
629 | 3. Protocols and services used
630 | 4. Notable events or anomalies
631 | 5. Potential security concerns`
632 | }
633 | }]
634 | })
635 | );
636 |
637 | server.prompt(
638 | 'extract_credentials_prompt',
639 | {
640 | pcapPath: z.string().describe('Path to the PCAP file'),
641 | },
642 | ({ pcapPath }) => ({
643 | messages: [{
644 | role: 'user',
645 | content: {
646 | type: 'text',
647 | text: `Please analyze the PCAP file at ${pcapPath} for potential credential exposure:
648 | 1. Look for plaintext credentials (HTTP Basic Auth, FTP, Telnet)
649 | 2. Identify Kerberos authentication attempts
650 | 3. Extract any hashed credentials
651 | 4. Provide security recommendations for credential handling`
652 | }
653 | }]
654 | })
655 | );
656 |
657 | // Start the server
658 | server.connect(new StdioServerTransport())
659 | .then(() => console.error('WireMCP Server is running...'))
660 | .catch(err => {
661 | console.error('Failed to start WireMCP:', err);
662 | process.exit(1);
663 | });
```