Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 118 additions & 55 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// index.js - WireMCP Server
const axios = require('axios');
const { exec } = require('child_process');
const { execFile } = require('child_process');
const { promisify } = require('util');
const which = require('which');
const fs = require('fs').promises;
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const { z } = require('zod');
Expand All @@ -13,6 +13,15 @@ const { z } = require('zod');
const originalConsoleLog = console.log;
console.log = (...args) => console.error(...args);

// Validate the network interface name
function validateInterface(interface) {
const validPattern = /^[A-Za-z0-9_.:\- ]+$/;
if (!validPattern.test(interface)) {
throw new Error(`Invalid network interface: ${interface}`);
}
return interface;
}

// Dynamically locate tshark
async function findTshark() {
try {
Expand All @@ -27,7 +36,7 @@ async function findTshark() {

for (const path of fallbacks) {
try {
await execAsync(`${path} -v`);
await execFileAsync(path, ['-v']);
console.error(`Found tshark at fallback: ${path}`);
return path;
} catch (e) {
Expand All @@ -49,7 +58,7 @@ server.tool(
'capture_packets',
'Capture live traffic and provide raw packet data as JSON for LLM analysis',
{
interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'),
duration: z.number().optional().default(5).describe('Capture duration in seconds'),
},
async (args) => {
Expand All @@ -59,15 +68,26 @@ server.tool(
const tempPcap = 'temp_capture.pcap';
console.error(`Capturing packets on ${interface} for ${duration}s`);

await execAsync(
`${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
await execFileAsync(
tsharkPath, ['-i', validateInterface(interface), '-w', tempPcap, '-a', `duration:${duration}`],
{env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);

const { stdout, stderr } = await execAsync(
`${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`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);
const { stdout, stderr } = await execFileAsync(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'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});
if (stderr) console.error(`tshark stderr: ${stderr}`);
let packets = JSON.parse(stdout);

Expand Down Expand Up @@ -101,7 +121,7 @@ server.tool(
'get_summary_stats',
'Capture live traffic and provide protocol hierarchy statistics for LLM analysis',
{
interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'),
duration: z.number().optional().default(5).describe('Capture duration in seconds'),
},
async (args) => {
Expand All @@ -111,15 +131,20 @@ server.tool(
const tempPcap = 'temp_capture.pcap';
console.error(`Capturing summary stats on ${interface} for ${duration}s`);

await execAsync(
`${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);

const { stdout, stderr } = await execAsync(
`${tsharkPath} -r "${tempPcap}" -qz io,phs`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);
await execFileAsync(tsharkPath, [
'-i', validateInterface(interface),
'-w', tempPcap,
'-a', `duration:${duration}`
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});

const { stdout, stderr } = await execFileAsync(tsharkPath, [
'-r', tempPcap,
'-qz', 'io,phs'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});
if (stderr) console.error(`tshark stderr: ${stderr}`);

await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`));
Expand All @@ -142,7 +167,7 @@ server.tool(
'get_conversations',
'Capture live traffic and provide TCP/UDP conversation statistics for LLM analysis',
{
interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'),
duration: z.number().optional().default(5).describe('Capture duration in seconds'),
},
async (args) => {
Expand All @@ -152,15 +177,20 @@ server.tool(
const tempPcap = 'temp_capture.pcap';
console.error(`Capturing conversations on ${interface} for ${duration}s`);

await execAsync(
`${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);

const { stdout, stderr } = await execAsync(
`${tsharkPath} -r "${tempPcap}" -qz conv,tcp`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);
await execFileAsync(tsharkPath, [
'-i', validateInterface(interface),
'-w', tempPcap,
'-a', `duration:${duration}`
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});

const { stdout, stderr } = await execFileAsync(tsharkPath, [
'-r', tempPcap,
'-qz', 'conv,tcp'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});
if (stderr) console.error(`tshark stderr: ${stderr}`);

await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`));
Expand All @@ -183,7 +213,7 @@ server.tool(
'check_threats',
'Capture live traffic and check IPs against URLhaus blacklist',
{
interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'),
interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'),
duration: z.number().optional().default(5).describe('Capture duration in seconds'),
},
async (args) => {
Expand All @@ -193,15 +223,22 @@ server.tool(
const tempPcap = 'temp_capture.pcap';
console.error(`Capturing traffic on ${interface} for ${duration}s to check threats`);

await execAsync(
`${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);

const { stdout } = await execAsync(
`${tsharkPath} -r "${tempPcap}" -T fields -e ip.src -e ip.dst`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);
await execFileAsync(tsharkPath, [
'-i', validateInterface(interface),
'-w', tempPcap,
'-a', `duration:${duration}`
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});

const { stdout } = await execFileAsync(tsharkPath, [
'-r', tempPcap,
'-T', 'fields',
'-e', 'ip.src',
'-e', 'ip.dst'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});
const ips = [...new Set(stdout.split('\n').flatMap(line => line.split('\t')).filter(ip => ip && ip !== 'unknown'))];
console.error(`Captured ${ips.length} unique IPs: ${ips.join(', ')}`);

Expand Down Expand Up @@ -314,10 +351,22 @@ server.tool(
await fs.access(pcapPath);

// Extract broad packet data
const { stdout, stderr } = await execAsync(
`${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`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);
const { stdout, stderr } = await execFileAsync(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'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});
if (stderr) console.error(`tshark stderr: ${stderr}`);
const packets = JSON.parse(stdout);

Expand Down Expand Up @@ -377,17 +426,31 @@ server.tool(
await fs.access(pcapPath);

// Extract plaintext credentials
const { stdout: plaintextOut } = await execAsync(
`${tsharkPath} -r "${pcapPath}" -T fields -e http.authbasic -e ftp.request.command -e ftp.request.arg -e telnet.data -e frame.number`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);
const { stdout: plaintextOut } = await execFileAsync(tsharkPath, [
'-r', pcapPath,
'-T', 'fields',
'-e', 'http.authbasic',
'-e', 'ftp.request.command',
'-e', 'ftp.request.arg',
'-e', 'telnet.data',
'-e', 'frame.number'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});

// Extract Kerberos credentials
const { stdout: kerberosOut } = await execAsync(
`${tsharkPath} -r "${pcapPath}" -T fields -e kerberos.CNameString -e kerberos.realm -e kerberos.cipher -e kerberos.type -e kerberos.msg_type -e frame.number`,
{ env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } }
);

const { stdout: kerberosOut } = await execFileAsync(tsharkPath, [
'-r', pcapPath,
'-T', 'fields',
'-e', 'kerberos.CNameString',
'-e', 'kerberos.realm',
'-e', 'kerberos.cipher',
'-e', 'kerberos.type',
'-e', 'kerberos.msg_type',
'-e', 'frame.number'
], {
env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` }
});
const lines = plaintextOut.split('\n').filter(line => line.trim());
const packets = lines.map(line => {
const [authBasic, ftpCmd, ftpArg, telnetData, frameNumber] = line.split('\t');
Expand Down Expand Up @@ -660,4 +723,4 @@ server.connect(new StdioServerTransport())
.catch(err => {
console.error('Failed to start WireMCP:', err);
process.exit(1);
});
});