Skip to main content
Glama
MIT License
25
89
  • Linux
  • Apple
index.ts.backup44.8 kB
#!/usr/bin/env node /// <reference path="./node-nmap.d.ts" /> // Import MCP SDK with standard import patterns import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; // import { PromptTemplate } from "@modelcontextprotocol/sdk/server.js"; // Removed - Cannot find module import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { UserMode, UserSession } from './types.js'; import nmap, { ScanData, Host, Port } from 'node-nmap'; import { logScanResult, logMessage, getLatestScanResultForTarget } from './logger.js'; import { spawn } from 'child_process'; import util from 'util'; import fs from 'fs/promises'; import { URL } from 'url'; import path from 'path'; // @ts-ignore - No types available for fast-xml-parser import { XMLParser } from 'fast-xml-parser'; let currentUserSession: UserSession = { mode: UserMode.UNKNOWN, // Start as unknown, will be determined or asked history: [], }; // Track running scans with their progress states interface ScanProgress { scanId: string; target: string; startTime: number; progress: number; // 0-100 status: 'initializing' | 'scanning' | 'analyzing' | 'complete' | 'failed' | 'cancelled'; currentStage?: string; estimatedTimeRemaining?: number; } // In the runNmapScan function const activeScans: Map<string, {process: any, progress: ScanProgress}> = new Map(); // Server instance - declared globally, initialized in main let server: McpServer; /* // Commenting out trackProgress as sendNotification signature/existence is uncertain in v1.7 function trackProgress(progress: ScanProgress) { if (server) { // server.sendNotification('scan/progress', { progress }); // Likely changed/removed console.log(`[Progress Update - Skipped Sending]: Scan ${progress.scanId} - ${progress.status} - ${progress.progress}%`); } else { // Queue for later if server isn't ready } } */ // 2. Implement getScanDataById function function getScanDataById(scanId: string): any { // First check active scans const activeScan = activeScans.get(scanId); if (activeScan) { return { scanId, target: activeScan.progress.target, options: [], // Would need to store this in the activeScan object timestamp: activeScan.progress.startTime, results: {} // Would need to store partial results }; } // Otherwise, would need to retrieve from log file // This is a placeholder - real implementation would parse logs return null; } // Define the structure of a finding interface Finding { host: string; port: string; service: string; description: string; details: Port; // Assuming Port is the type from 'node-nmap' } // 3. Implement analyzeFindings function function analyzeFindings(scans: any[]): { critical: Finding[]; high: Finding[]; medium: Finding[]; low: Finding[]; info: Finding[]; } { const findings: { critical: Finding[]; high: Finding[]; medium: Finding[]; low: Finding[]; info: Finding[]; } = { critical: [], high: [], medium: [], low: [], info: [] }; for (const scan of scans) { if (!scan || !scan.results) continue; for (const ip in scan.results) { const host = scan.results[ip] as Host; if (!host.ports) continue; for (const port of host.ports) { if (port.state === 'open') { const finding: Finding = { host: ip, port: port.portId, service: port.service?.name || 'unknown', description: `Open port ${port.portId} (${port.service?.name || 'unknown'})`, details: port }; if (['3389', '5432', '1433', '21', '23'].includes(port.portId)) { findings.critical.push(finding); } else if (['22', '445', '139'].includes(port.portId)) { findings.high.push(finding); } else if (['80', '443', '8080'].includes(port.portId)) { findings.medium.push(finding); } else { findings.low.push(finding); } } } } } return findings; } // --- Nmap Execution Logic (with spawn and XML parsing) --- async function runNmapScan(target: string, options: string[] = []): Promise<ScanData | null> { const scanId = `scan-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const progress: ScanProgress = { scanId, target, startTime: Date.now(), progress: 0, status: 'initializing' }; // trackProgress(progress); // Commented out console.log(`Executing nmap scan: target=${target}, options=${options.join(' ')}`); if (!target) { throw new Error("Error: Target is required."); } return new Promise((resolve, reject) => { const args = [...options, '-oX', '-', target]; const nmapProcess = spawn('nmap', args); activeScans.set(scanId, { process: nmapProcess, progress }); // Track active scan let stdoutData = ''; let stderrData = ''; nmapProcess.stdout.on('data', (data) => { stdoutData += data.toString(); }); nmapProcess.stderr.on('data', (data) => { stderrData += data.toString(); // Update progress based on stderr const output = data.toString(); if (output.includes('Initiating')) { progress.status = 'scanning'; progress.progress = 10; } else if (output.includes('Completed SYN Stealth Scan')) { progress.progress = 40; } else if (output.includes('Initiating Service scan')) { progress.currentStage = 'Service detection'; progress.progress = 50; } else if (output.includes('Completed Service scan')) { progress.progress = 70; } else if (output.includes('Initiating OS detection')) { progress.currentStage = 'OS detection'; progress.progress = 80; } // trackProgress(progress); // Commented out }); nmapProcess.on('error', (error) => { activeScans.delete(scanId); progress.status = 'failed'; // trackProgress(progress); // Commented out reject(error); }); let parsedScanData: ScanData | null = null; nmapProcess.on('close', async (code) => { activeScans.delete(scanId); console.log(`Nmap process exited with code ${code}`); if (stderrData) { console.warn(`Nmap stderr: ${stderrData}`); } let resultForLog: ScanData | string | null = null; if (code === 0 && stdoutData) { try { const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@", allowBooleanAttributes: true, parseAttributeValue: true, isArray: (name: string) => ['host', 'port', 'hostname', 'osmatch', 'cpe'].includes(name) }); const parsedXml = parser.parse(stdoutData); parsedScanData = {}; // Init // ... (Transformation logic) ... resultForLog = parsedScanData; // Log the parsed data on success } catch (parseError: any) { console.error("Failed to parse Nmap XML output:", parseError); resultForLog = `XML Parse Error: ${parseError.message}\n${stdoutData}`; // Log error + raw data parsedScanData = null; // Don't log twice, the main log call handles this based on code reject(new Error(`Failed to parse Nmap XML output: ${parseError.message}`)); return; } } else if (code !== 0) { resultForLog = stderrData || `Nmap failed: ${code}`; // Log stderr on failure } if (currentUserSession.mode === UserMode.PROFESSIONAL) { // Correct call with 3 arguments await logScanResult(target, options, resultForLog); } if (code !== 0) { const errorMsg = `Nmap scan failed with exit code ${code}.${stderrData ? " Stderr: " + stderrData : ""}`; reject(new Error(errorMsg)); } else { resolve(parsedScanData); // Resolve with parsed data (or null) } }); }); } // --- Input Sanitization Helper --- const SAFE_OPTION_REGEX = /^(?:-[a-zA-Z0-9]+|--[a-zA-Z0-9\-]+(?:=[^;&|`$\s\(\)\<\>\\]+)?|[^;&|`$\s\(\)\<\>\\]+)$/; function sanitizeOptions(options: string[]): string[] { const sanitized: string[] = []; for (const opt of options) { if (SAFE_OPTION_REGEX.test(opt)) { sanitized.push(opt); } else { throw new Error(`Invalid or potentially unsafe option detected: "${opt}". Only standard flags and simple arguments are allowed.`); } } return sanitized; } // Helper function to run a command using spawn and return results async function runSpawnCommand(command: string, args: string[]): Promise<{ stdout: string; stderr: string; code: number | null }> { return new Promise((resolve, reject) => { console.log(`Attempting to spawn: ${command} ${args.join(' ')}`); // Added for debugging const process = spawn(command, args); let stdout = ''; let stderr = ''; process.stdout.on('data', (data) => { stdout += data.toString(); }); process.stderr.on('data', (data) => { stderr += data.toString(); }); process.on('error', (error) => { // Explicitly catch spawn errors (e.g., command not found) console.error(`Spawn error for command "${command}": ${error.message}`); reject(new Error(`Failed to start command "${command}": ${error.message}`)); }); process.on('close', (code) => { console.log(`Command "${command}" exited with code ${code}`); // Added for debugging resolve({ stdout, stderr, code }); }); }); } // --- John the Ripper Execution Logic (using spawn) --- async function runJtR(hashData: string, rawOptions: string[] = []): Promise<{ fullOutput: string; cracked: string[] }> { let options: string[]; try { options = sanitizeOptions(rawOptions); } catch (error: any) { throw error; } if (!hashData) { throw new Error("Error: Hash data is required."); } const tempHashFile = `jtr_hashes_${Date.now()}.txt`; let fullOutput = ""; let crackedPasswords: string[] = []; try { await fs.writeFile(tempHashFile, hashData); const crackingArgs = [...options, tempHashFile]; fullOutput += `--- Cracking Attempt ---\nExecuting: john ${crackingArgs.join(' ')}\n`; try { const crackResult = await runSpawnCommand('john', crackingArgs); fullOutput += `Exit Code: ${crackResult.code}\nStdout:\n${crackResult.stdout}\nStderr:\n${crackResult.stderr}\n`; } catch (error: any) { fullOutput += `Cracking command failed to execute: ${error.message}\n`; } const showArgs = ['--show', tempHashFile]; fullOutput += `--- Show Attempt ---\nExecuting: john ${showArgs.join(' ')}\n`; try { const showResult = await runSpawnCommand('john', showArgs); fullOutput += `Exit Code: ${showResult.code}\nStdout:\n${showResult.stdout}\nStderr:\n${showResult.stderr}\n`; crackedPasswords = showResult.stdout.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('0 passwords cracked') && !line.includes('guesses remaining')); } catch (error: any) { fullOutput += `Show command failed to execute: ${error.message}\n`; } await fs.unlink(tempHashFile); if (currentUserSession.mode === UserMode.PROFESSIONAL) { await logMessage(`Ran John the Ripper.\nOptions: ${options.join(' ')}\nCracked: ${crackedPasswords.length}.`); } return { fullOutput, cracked: crackedPasswords }; } catch (error: any) { console.error("Fatal error setting up John the Ripper execution:", error); try { await fs.unlink(tempHashFile); } catch { /* ignore */ } if (currentUserSession.mode === UserMode.PROFESSIONAL) { await logMessage(`John the Ripper FAILED fatally before execution.\nOptions: ${options.join(' ')}\nError: ${error.message}`); } throw new Error(`John the Ripper setup failed fatally: ${error.message}`); } } // --- Student Mode Response Formatting --- function formatResultsForStudent(target: string, options: string[], results: ScanData): { explanation: string, suggestions: string[] } { let explanation = `Scan results for **${target}** (options: ${options.join(' ') || 'default'}):\n\n`; const suggestions: string[] = []; let foundOpenPorts = false; for (const ip in results) { const hostData = results[ip] as Host; explanation += `**Host:** ${hostData.hostname ? `${hostData.hostname} (${ip})` : ip}\n`; if (hostData.mac) { explanation += `* MAC Address: ${hostData.mac} (This is the hardware address, useful for identifying devices on a local network).\n`; } if (hostData.osNmap) { explanation += `* Operating System Guess: ${hostData.osNmap} (Nmap tries to guess the OS based on network responses).\n`; if (!options.includes('-O')) { suggestions.push(`Try adding \`-O\` to the options for a more dedicated OS detection scan on ${ip}.`); } } if (hostData.ports && hostData.ports.length > 0) { const openPorts = hostData.ports.filter(port => port.state === 'open'); if (openPorts.length > 0) { foundOpenPorts = true; explanation += `* **Open Ports Found:**\n`; openPorts.forEach(port => { explanation += ` * **Port ${port.portId}/${port.protocol}:** State is **${port.state}**. Service detected: **${port.service?.name || 'unknown'}**. Reason: ${port.reason}\n`; if (port.portId === '80' || port.portId === '443') { suggestions.push(`Port ${port.portId} (${port.service?.name}) is open on ${ip}. Try exploring it with a web browser or tools like \`curl\`.`); suggestions.push(`Consider running \`nmapScan\` with scripts: \`options: ["-sV", "-sC", "-p${port.portId}"]\` on ${ip} to get more service info.`); } if (port.portId === '22') { suggestions.push(`SSH (Port 22) is open on ${ip}. You could try connecting if you have credentials, or check for common vulnerabilities (\`options: ["-sV", "--script=ssh-auth-methods"]\`).`); } if (port.portId === '21' || port.portId === '23') { // FTP/Telnet suggestions.push(`${port.service?.name} (Port ${port.portId}) on ${ip} is often insecure. Check for anonymous login or default credentials (\`options: ["-sV", "--script=ftp-anon"]\` for FTP).`); } if (port.portId === '3389') { // RDP suggestions.push(`RDP (Port 3389) on ${ip} allows remote desktop access. Check for weak passwords or vulnerabilities.`); } }); } else { explanation += `* No *open* ports were detected in the scanned range for ${ip}. Filtered ports might still exist.\n`; } } else { explanation += `* Port scanning was not performed or no ports were reported for ${ip}.\n`; } explanation += `\n`; } if (!foundOpenPorts) { suggestions.push("No open ports found with the current options. Try scanning all ports (\`-p-\` ) or using different scan types like SYN scan (\`-sS\`, requires root/admin) or UDP scan (\`-sU\`)."); } if (!options.includes('-sV') && foundOpenPorts) { suggestions.push("Run with \`-sV\` option to try and determine the version of the services running on the open ports."); } return { explanation, suggestions }; } const TEMP_WORDLIST_DIR = path.join(process.cwd(), 'temp_wordlists'); async function ensureTempWordlistDirExists(): Promise<void> { try { await fs.mkdir(TEMP_WORDLIST_DIR, { recursive: true }); } catch (error) { console.error('Error creating temp wordlist directory:', error); } } function toLeet(word: string): string { return word.replace(/a/gi, '4').replace(/e/gi, '3').replace(/i/gi, '1').replace(/o/gi, '0').replace(/s/gi, '5').replace(/t/gi, '7'); } // --- Gobuster Execution Logic (Reinstated) --- async function runGobuster(target: string, rawOptions: string[] = []): Promise<{ fullOutput: string; foundPaths: string[] }> { console.log(`Executing Gobuster: target=${target}, raw_options=${rawOptions.join(' ')}`); if (!target.startsWith('http://') && !target.startsWith('https://')) { throw new Error("Target must be a valid URL starting with http:// or https://"); } let options: string[]; try { options = sanitizeOptions(rawOptions); } catch (error: any) { throw error; } let fullOutput = ""; let foundPaths: string[] = []; try { const baseArgs = ['dir', '-u', target, ...options]; fullOutput += `--- Directory Enumeration ---\nExecuting: gobuster ${baseArgs.join(' ')}\n`; try { const result = await runSpawnCommand('gobuster', baseArgs); fullOutput += `Exit Code: ${result.code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}\n`; foundPaths = result.stdout.split('\n').filter(line => line.includes('Status:') && (line.includes('200') || line.includes('301') || line.includes('302'))).map(line => line.match(/^\s*(\/[^\s]*)/)?.[1] || line); } catch (error: any) { fullOutput += `Gobuster command failed to execute: ${error.message}\n`; } if (currentUserSession.mode === UserMode.PROFESSIONAL) { await logMessage(`Ran Gobuster against ${target}.\nOptions: ${options.join(' ')}\nFound: ${foundPaths.length} paths.`); } return { fullOutput, foundPaths }; } catch (error: any) { console.error("Fatal error setting up Gobuster execution:", error); if (currentUserSession.mode === UserMode.PROFESSIONAL) { await logMessage(`Gobuster FAILED fatally before execution.\nTarget: ${target}\nOptions: ${options.join(' ')}\nError: ${error.message}`); } throw new Error(`Gobuster setup failed fatally: ${error.message}`); } } // --- Nikto Execution Logic (Reinstated) --- async function runNikto(target: string, rawOptions: string[] = []): Promise<{ fullOutput: string; findings: string[] }> { console.log(`Executing Nikto: target=${target}, raw_options=${rawOptions.join(' ')}`); if (!target.startsWith('http://') && !target.startsWith('https://')) { throw new Error("Target must be a valid URL starting with http:// or https://"); } let options: string[]; try { options = sanitizeOptions(rawOptions); } catch (error: any) { throw error; } let fullOutput = ""; let findings: string[] = []; try { const baseArgs = ['-h', target, ...options]; fullOutput += `--- Vulnerability Scan ---\nExecuting: nikto ${baseArgs.join(' ')}\n`; try { const result = await runSpawnCommand('nikto', baseArgs); fullOutput += `Exit Code: ${result.code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}\n`; findings = result.stdout.split('\n').filter(line => line.startsWith('+') && !line.includes('+ No web server')).map(line => line.trim()); } catch (error: any) { fullOutput += `Nikto command failed to execute: ${error.message}\n`; } if (currentUserSession.mode === UserMode.PROFESSIONAL) { await logMessage(`Ran Nikto against ${target}.\nOptions: ${options.join(' ')}\nFound: ${findings.length} potential issues.`); } return { fullOutput, findings }; } catch (error: any) { console.error("Fatal error setting up Nikto execution:", error); if (currentUserSession.mode === UserMode.PROFESSIONAL) { await logMessage(`Nikto FAILED fatally before execution.\nTarget: ${target}\nOptions: ${options.join(' ')}\nError: ${error.message}`); } throw new Error(`Nikto setup failed fatally: ${error.message}`); } } // --- MCP Server Setup --- async function main() { // Log the PATH environment variable as seen by Node.js console.error('Node.js process PATH:', process.env.PATH); console.error('Initializing Pentest MCP Server...'); server = new McpServer({ name: "pentest-mcp", version: "0.2.6", // Explicitly declare capabilities to enable discovery capabilities: { resources: { "mode": {}, "clientReport": {} }, tools: { "setMode": {}, "generateWordlist": {}, "cancelScan": {}, "createClientReport": {} // nmapScan, runJohnTheRipper, gobuster, nikto are currently commented out }, // prompts: {} // No prompts defined } }); // --- Resource Definitions --- server.resource( "mode", new ResourceTemplate("mcp://pentest/mode", { list: undefined }), async (uri: URL /*, extra */) => { // Handler receives URI return { contents: [{ uri: uri.href, text: `Current Mode: ${currentUserSession.mode}`, metadata: { currentMode: currentUserSession.mode } }] }; } ); interface ClientReport { reportId: string; title: string; createdAt: number; client: string; assessmentType: string; findings: { critical: Finding[], high: Finding[], medium: Finding[], low: Finding[], info: Finding[] }; scans: any[]; summary: string; recommendations: string[]; } const clientReports: Map<string, ClientReport> = new Map(); server.resource( "clientReport", new ResourceTemplate("mcp://pentest/clientReport/{reportId}", { list: undefined }), async (uri: URL /*, extra */) => { const match = uri.href.match(/mcp:\/\/pentest\/clientReport\/(.+)/); const reportId = match ? match[1] : null; if (!reportId || reportId === "list") { return { contents: Array.from(clientReports.values()).map(report => ({ uri: `mcp://pentest/clientReport/${report.reportId}`, text: `Report: ${report.title}` })) }; } const report = clientReports.get(reportId); if (!report) throw new Error(`Report ${reportId} not found`); return { contents: [{ uri: uri.href, text: JSON.stringify(report, null, 2) }] }; } ); // --- Tool Definitions --- // Set Mode Tool const setModeToolSchema = z.object({ mode: z.enum([UserMode.STUDENT, UserMode.PROFESSIONAL]) }).describe( "Switch between `student` mode (verbose guidance) and `professional` mode " + "(concise output). Call this at the start of a session or whenever you " + "need to adjust the level of explanation. Example: `{\"mode\":\"professional\"}`" ); server.tool("setMode", setModeToolSchema.shape, async ({ mode } /*, extra */) => { currentUserSession.mode = mode; await logMessage(`Mode changed to ${mode}.`); return { content: [{ type: "text", text: `Session mode set to: ${mode}` }] }; }); // Nmap Scan Tool const nmapScanToolSchema = z.object({ target: z.string(), ports: z.string().optional(), fastScan: z.boolean().optional(), topPorts: z.number().int().optional(), scanTechnique: z.enum(['SYN', 'Connect', 'ACK', 'Window', 'Maimon', 'FIN', 'Xmas', 'Null', 'Proto']).optional(), udpScan: z.boolean().optional(), serviceVersionDetection: z.boolean().optional(), versionIntensity: z.number().int().optional(), osDetection: z.boolean().optional(), defaultScripts: z.boolean().optional(), scripts: z.array(z.string()).optional(), scriptArgs: z.string().optional(), timingTemplate: z.enum(['T0', 'T1', 'T2', 'T3', 'T4', 'T5']).optional(), skipHostDiscovery: z.boolean().optional(), verbose: z.boolean().optional(), rawOptions: z.array(z.string()).optional(), userModeHint: z.enum([UserMode.STUDENT, UserMode.PROFESSIONAL]).optional() }).describe( "Run an Nmap scan to discover hosts and services. Use this before other " + "tools to identify attack surface. Options map directly to Nmap flags. " + "Note that SYN scans or OS detection (e.g. `-sS`, `-O`) require elevated " + "privileges. Example: `{\"target\":\"192.168.1.0/24\", \"scanTechnique\":\"SYN\", \"serviceVersionDetection\":true}`" ); server.tool("nmapScan", nmapScanToolSchema.shape, async (args /*: z.infer<typeof nmapScanToolSchema> */ /*, extra */) => { const { target, ports, fastScan, topPorts, scanTechnique, udpScan, serviceVersionDetection, versionIntensity, osDetection, defaultScripts, scripts, scriptArgs, timingTemplate, skipHostDiscovery, verbose, rawOptions, userModeHint } = args; console.log(`Received nmapScan request:`, args); if (currentUserSession.mode === UserMode.UNKNOWN) { if (userModeHint) { currentUserSession.mode = userModeHint; /* log */ } else { currentUserSession.mode = UserMode.STUDENT; /* log */ } } try { const constructedOptions: string[] = []; const validationErrors: string[] = []; // Restore detailed option building logic if (skipHostDiscovery) constructedOptions.push('-Pn'); let portSpecCount = 0; if (ports) portSpecCount++; if (fastScan) portSpecCount++; if (topPorts) portSpecCount++; if (portSpecCount > 1) validationErrors.push("Use only one of ports, fastScan, or topPorts."); else if (ports) constructedOptions.push('-p', ports); else if (fastScan) constructedOptions.push('-F'); else if (topPorts) constructedOptions.push('--top-ports', String(topPorts)); if (scanTechnique) { switch (scanTechnique) { case 'SYN': constructedOptions.push('-sS'); break; case 'Connect': constructedOptions.push('-sT'); break; case 'ACK': constructedOptions.push('-sA'); break; case 'Window': constructedOptions.push('-sW'); break; case 'Maimon': constructedOptions.push('-sM'); break; case 'FIN': constructedOptions.push('-sF'); break; case 'Xmas': constructedOptions.push('-sX'); break; case 'Null': constructedOptions.push('-sN'); break; case 'Proto': constructedOptions.push('-sO'); break; } } if (udpScan) constructedOptions.push('-sU'); if (serviceVersionDetection) { constructedOptions.push('-sV'); if (versionIntensity !== undefined) constructedOptions.push('--version-intensity', String(versionIntensity)); } else if (versionIntensity !== undefined) validationErrors.push("Cannot set intensity without -sV."); if (osDetection) constructedOptions.push('-O'); if (defaultScripts && scripts) validationErrors.push("Cannot use both -sC and --script."); else if (defaultScripts) constructedOptions.push('-sC'); else if (scripts && scripts.length > 0) constructedOptions.push('--script', scripts.join(',')); if (scriptArgs) { if (!defaultScripts && !scripts) validationErrors.push("Cannot use scriptArgs without scripts."); else constructedOptions.push('--script-args', scriptArgs); } if (timingTemplate) constructedOptions.push(`-${timingTemplate}`); if (verbose) constructedOptions.push('-v'); if (rawOptions) constructedOptions.push(...rawOptions); if (validationErrors.length > 0) throw new Error(`Invalid params: ${validationErrors.join('; ')}`); // Privilege Warning (remains valid) const needsPrivileges = constructedOptions.some(opt => opt === '-sS' || opt === '-O'); if (needsPrivileges) console.warn("Nmap options may require elevated privileges."); const results = await runNmapScan(target, constructedOptions); // Restore detailed response formatting let responseContent: any[] = []; let suggestions: string[] = []; if (typeof results === 'string') { responseContent.push({ type: "text", text: results }); } else if (results) { if (currentUserSession.mode === UserMode.STUDENT) { const { explanation, suggestions: studentSuggestions } = formatResultsForStudent(target, constructedOptions, results); responseContent.push({ type: "text", text: explanation }); suggestions.push(...studentSuggestions); } else { // Professional Mode responseContent.push({ type: "text", text: JSON.stringify(results, null, 2) }); const foundPorts: { [key: string]: Set<string> } = {}; Object.entries(results).forEach(([ip, host]) => { const typedHost = host as nmap.Host; if (!foundPorts[ip]) foundPorts[ip] = new Set(); typedHost.ports?.forEach(port => { if (port.state === 'open') foundPorts[ip].add(port.portId); }); }); for (const ip in foundPorts) { /* ... pro suggestions logic ... */ } if (suggestions.length === 0 && Object.keys(foundPorts).length > 0) suggestions.push("Scan complete."); } } else { responseContent.push({ type: "text", text: "Nmap scan returned no data." }); } if (suggestions.length > 0) responseContent.push({ type: "text", text: "\n**Suggestions:**\n* " + suggestions.join("\n* ") }); return { content: responseContent }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // Generate Wordlist Tool const generateWordlistSchema = z.object({ baseWords: z.array(z.string()).describe("List of base words (names, pets, places, etc.)."), dates: z.array(z.string()).optional().describe("List of dates (YYYY-MM-DD, MM-DD, YYYY). Parsed for variations."), customPatterns: z.array(z.string()).optional().describe("List of custom patterns/symbols to prepend/append (e.g., '!', '123')."), minYear: z.number().int().optional().describe("Minimum year (YYYY) to include in variations."), maxYear: z.number().int().optional().describe("Maximum year (YYYY) to include in variations (defaults to current year)."), includeLeet: z.boolean().optional().default(false).describe("Apply basic leetspeak substitutions (a=4, e=3, etc.)."), caseVariations: z.boolean().optional().default(true).describe("Include variations like TitleCase, UPPERCASE.") }).describe( "Generate a custom password wordlist from target-related words. " + "Use this before running John the Ripper. Example: `{\"baseWords\":[\"Acme\",\"Smith\"],\"dates\":[\"1984\"],\"customPatterns\":[\"!\"]}`" ); server.tool("generateWordlist", generateWordlistSchema.shape, async ({ baseWords, dates, customPatterns, minYear, maxYear, includeLeet, caseVariations } /*, extra */) => { console.log(`Received generateWordlist:`, { baseWords: `${baseWords.length} words`, dates, customPatterns }); await ensureTempWordlistDirExists(); const wordlist = new Set<string>(); const currentYear = new Date().getFullYear(); const resolvedMinYear = minYear || currentYear - 10; const resolvedMaxYear = maxYear || currentYear; const years: string[] = []; for (let y = resolvedMinYear; y <= resolvedMaxYear; y++) { years.push(String(y)); years.push(String(y).slice(-2)); } const dateVariations: string[] = []; if (dates) { dates.forEach(dateStr => { const parts = dateStr.split(/[-/]/); if (parts.length === 3) { dateVariations.push(parts[0], parts[0].slice(-2), parts[1], parts[2], parts[1]+parts[2], parts[1]+parts[2]+parts[0], parts[1]+parts[2]+parts[0].slice(-2)); } else if (parts.length === 2) { dateVariations.push(parts[0], parts[1], parts[0]+parts[1]); } else if (parts.length === 1 && parts[0].length === 4) { dateVariations.push(parts[0], parts[0].slice(-2)); } }); } const patterns = customPatterns || []; const suffixes = [...patterns, ...years, ...dateVariations]; const prefixes = [...patterns]; baseWords.forEach(base => { const variations = new Set<string>([base]); if (caseVariations) { variations.add(base.toLowerCase()); variations.add(base.toUpperCase()); variations.add(base.charAt(0).toUpperCase() + base.slice(1).toLowerCase()); } if (includeLeet) { const leetBase = toLeet(base); variations.add(leetBase); if (caseVariations) { /* add leet cases */ variations.add(leetBase.toLowerCase()); variations.add(leetBase.toUpperCase()); variations.add(leetBase.charAt(0).toUpperCase() + leetBase.slice(1).toLowerCase()); } } variations.forEach(v => { wordlist.add(v); prefixes.forEach(p => { wordlist.add(p + v); }); suffixes.forEach(s => { wordlist.add(v + s); }); prefixes.forEach(p => { suffixes.forEach(s => { wordlist.add(p + v + s); }); }); }); }); [...years, ...dateVariations].forEach(dv => wordlist.add(dv)); const filename = `wordlist_${Date.now()}.txt`; const filePath = path.join(TEMP_WORDLIST_DIR, filename); try { await fs.writeFile(filePath, Array.from(wordlist).join('\n')); return { content: [ { type: "text", text: `Generated ${wordlist.size} words.` }, { type: "text", text: `Path: ${filePath}` } ] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // John the Ripper Tool const jtrToolSchema = z.object({ hashData: z.string().describe("String containing the password hashes, one per line."), options: z.array(z.string()).optional().describe("Array of command-line options for JtR.") }).describe( "Crack password hashes using John the Ripper. Provide hashes and any JtR options." + " Run this after generating a wordlist. Example: `{\"hashData\":\"user:$1$hash\", \"options\":[\"--wordlist=/tmp/list.txt\"]}`" ); server.tool("runJohnTheRipper", jtrToolSchema.shape, async ({ hashData, options } /*, extra */) => { console.log(`Received JtR:`, { hashData: `len=${hashData.length}`, options }); if (currentUserSession.mode === UserMode.STUDENT) console.warn("[Student Mode] Executing JtR."); try { const { fullOutput, cracked } = await runJtR(hashData, options || []); const responseContent: any[] = [ { type: "text", text: `JtR finished. Found ${cracked.length} cracked.` } ]; if (cracked.length > 0) responseContent.push({ type: "text", text: "\n**Cracked:**\n" + cracked.join("\n") }); responseContent.push({ type: "text", text: "\n--- Full JtR Output ---\n" + fullOutput }); // Keep full output return { content: responseContent }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // Cancel Scan Tool const cancelScanToolSchema = z.object({ scanId: z.string().describe("The ID of the scan to cancel") }).describe( "Stop an Nmap scan that is currently running. Pass the scanId returned when the scan was started." ); server.tool("cancelScan", cancelScanToolSchema.shape, async ({ scanId } /*, extra */) => { const activeScan = activeScans.get(scanId); if (!activeScan) { return { content: [{ type: "text", text: `Error: No active scan found with ID: ${scanId}` }], isError: true }; } try { activeScan.process.kill(); activeScan.progress.status = 'cancelled'; activeScans.delete(scanId); return { content: [{ type: "text", text: `Successfully cancelled scan ${scanId}` }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: Failed to cancel scan: ${error.message}` }], isError: true }; } }); // Create Client Report Tool const createClientReportToolSchema = z.object({ client: z.string().describe("Client name for the report"), title: z.string().describe("Title of the assessment report"), assessmentType: z.string().describe("Type of assessment"), scanIds: z.array(z.string()).describe("IDs of scans to include"), summary: z.string().optional().describe("Executive summary"), recommendations: z.array(z.string()).optional().describe("List of recommendations") }).describe( "Create a summarized client report from completed scans. Include scanIds returned from tools like `nmapScan`." ); server.tool("createClientReport", createClientReportToolSchema.shape, async (args /*: z.infer<typeof createClientReportToolSchema> */ /*, extra */) => { const { client, title, assessmentType, scanIds, summary, recommendations } = args; const reportId = `report-${Date.now()}`; const scans = scanIds.map(getScanDataById).filter(Boolean); const findings = analyzeFindings(scans); const report: ClientReport = { reportId, title, client, createdAt: Date.now(), assessmentType, findings, scans, summary: summary || "", recommendations: recommendations || [] }; clientReports.set(reportId, report); return { content: [ { type: "text", text: `Client report created: ${reportId}` }, { type: "text", text: `URI: mcp://pentest/clientReport/${reportId}` } ] }; }); // Gobuster Tool (Reinstated) const gobusterToolSchema = z.object({ target: z.string().url().describe("Target URL"), wordlist: z.string().describe("Path to wordlist"), extensions: z.string().optional().describe("File extensions (comma-separated)"), threads: z.number().int().positive().optional().describe("Number of threads"), statusCodes: z.string().optional().describe("Valid status codes (comma-separated)"), useragent: z.string().optional().describe("User-Agent string"), timeout: z.string().optional().describe("Timeout for requests"), basicAuth: z.string().optional().describe("Basic authentication credentials (username:password)"), cookie: z.string().optional().describe("Cookie to include in requests"), excludeLength: z.array(z.number()).optional().describe("Exclude paths of specific lengths"), followRedirect: z.boolean().optional().describe("Follow HTTP redirects"), noTLSValidation: z.boolean().optional().describe("Skip TLS certificate validation"), rawOptions: z.array(z.string()).optional().describe("Raw gobuster options") }).describe( "Enumerate hidden directories and files on a web server. Run this after confirming the target hosts a web service. " + "Provide a wordlist and optional extensions. Example: `{\"target\":\"http://example.com\", \"wordlist\":\"/usr/share/wordlists/common.txt\"}`" ); server.tool("gobuster", gobusterToolSchema.shape, async (args /*: z.infer<typeof gobusterToolSchema> */ /*, extra */) => { const { target, wordlist, extensions, threads, statusCodes, useragent, timeout, basicAuth, cookie, excludeLength, followRedirect, noTLSValidation, rawOptions } = args; console.log(`Received gobuster request:`, args); try { const constructedOptions: string[] = []; constructedOptions.push('-w', wordlist); if (extensions) constructedOptions.push('-x', extensions); if (threads) constructedOptions.push('-t', threads.toString()); if (statusCodes) constructedOptions.push('-s', statusCodes); if (useragent) constructedOptions.push('-a', useragent); if (timeout) constructedOptions.push('--timeout', timeout); if (basicAuth) { const [u,p] = basicAuth.split(':'); constructedOptions.push('-U', u, '-P', p); } if (cookie) constructedOptions.push('-c', cookie); if (excludeLength) constructedOptions.push('-l', excludeLength.join(',')); if (followRedirect) constructedOptions.push('-r'); if (noTLSValidation) constructedOptions.push('-k'); if (rawOptions) constructedOptions.push(...rawOptions); const { fullOutput, foundPaths } = await runGobuster(target, constructedOptions); const responseContent: any[] = []; if (currentUserSession.mode === UserMode.STUDENT) { responseContent.push({ type: "text", text: `Found ${foundPaths.length} paths/files at ${target}:\n\n${foundPaths.join('\n')}` }); if (foundPaths.length > 0) responseContent.push({ type: "text", text: "\n**Next Steps:** ..." }); else responseContent.push({ type: "text", text: "\n**No paths found...**" }); } else { responseContent.push({ type: "text", text: `Found ${foundPaths.length} paths at ${target}` }); if (foundPaths.length > 0) responseContent.push({ type: "text", text: "\n**Paths:**\n" + foundPaths.join('\n') }); responseContent.push({ type: "text", text: "\n**Full Output:**\n" + fullOutput }); } return { content: responseContent }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // Nikto Tool (Reinstated) const niktoToolSchema = z.object({ target: z.string().url().describe("Target URL"), port: z.string().optional().describe("Port(s) to scan"), ssl: z.boolean().optional().describe("Force SSL mode"), timeout: z.number().int().optional().describe("Timeout for requests"), useragent: z.string().optional().describe("User-Agent string"), tuning: z.string().optional().describe("Tuning mode"), output: z.string().optional().describe("Output file"), proxy: z.string().optional().describe("Use proxy"), basicAuth: z.string().optional().describe("Basic authentication credentials (username:password)"), root: z.string().optional().describe("Root directory"), cookies: z.string().optional().describe("Cookies to include"), rawOptions: z.array(z.string()).optional().describe("Raw nikto options") }).describe( "Scan a web server for common vulnerabilities with Nikto. Use after identifying a live site. " + "Some checks may be noisy. Example: `{\"target\":\"http://192.168.1.10\", \"ssl\":true, \"port\":\"443\"}`" ); server.tool("nikto", niktoToolSchema.shape, async (args /*: z.infer<typeof niktoToolSchema> */ /*, extra */) => { const { target, port, ssl, timeout, useragent, tuning, output, proxy, basicAuth, root, cookies, rawOptions } = args; console.log(`Received nikto request:`, args); try { const constructedOptions: string[] = []; constructedOptions.push('-nointeractive'); if (port) constructedOptions.push('-p', port); if (ssl) constructedOptions.push('-ssl'); if (timeout) constructedOptions.push('-Timeout', timeout.toString()); if (useragent) constructedOptions.push('-useragent', useragent); if (tuning) constructedOptions.push('-Tuning', tuning); if (output) constructedOptions.push('-o', output); if (proxy) constructedOptions.push('-useproxy', proxy); if (basicAuth) { constructedOptions.push('-id', basicAuth); } if (root) constructedOptions.push('-root', root); if (cookies) constructedOptions.push('-Cookies', cookies); if (rawOptions) constructedOptions.push(...rawOptions); const { fullOutput, findings } = await runNikto(target, constructedOptions); const responseContent: any[] = []; if (currentUserSession.mode === UserMode.STUDENT) { responseContent.push({ type: "text", text: `Found ${findings.length} issues at ${target}` }); if (findings.length > 0) { /* Categorize and add explanations */ } else { responseContent.push({ type: "text", text: "\n**No significant issues found...**" }); } } else { responseContent.push({ type: "text", text: `Found ${findings.length} issues at ${target}` }); if (findings.length > 0) responseContent.push({ type: "text", text: "\n**Findings:**\n- " + findings.join('\n- ') }); responseContent.push({ type: "text", text: "\n**Full Output:**\n" + fullOutput }); if (findings.some(f => f.toLowerCase().includes('injection'))) { /* Suggest follow-up */ } } return { content: responseContent }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // --- Start Server --- const transport = new StdioServerTransport(); await server.connect(transport); console.error('Pentest MCP Server connected and listening.'); // Keep the process alive indefinitely for stdio transport await new Promise(() => {}); // This promise never resolves // This part will likely not be reached in stdio mode unless the above promise is changed console.error('Pentest MCP Server shutdown.'); } main().catch(error => { console.error("Unhandled error in main:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DMontgomery40/pentest-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server