pentest-mcp
Version:
NOT for educational use: An MCP server for Nmap and John the Ripper, for professional penetration testers. Supports stdio, HTTP, and SSE transports with OAuth 2.1 authentication.
133 lines (115 loc) • 5.63 kB
text/typescript
/// <reference path="./node-nmap.d.ts" />
import fs from 'fs/promises';
import path from 'path';
import { ScanData, Host } from 'node-nmap'; // Use types from our d.ts file
// Use /tmp directory for logs to avoid read-only directory issues
const LOG_DIR = path.join('/tmp', 'pentest-mcp-logs');
const LOG_FILE = path.join(LOG_DIR, 'scan_report.md');
// Ensure the log directory exists
async function ensureLogDirExists(): Promise<void> {
try {
await fs.mkdir(LOG_DIR, { recursive: true });
} catch (error) {
console.error('Error creating log directory:', error);
// Depending on requirements, might want to throw or handle differently
}
}
// Helper to format nmap ScanData into a user-friendly markdown string
function formatScanDataToMarkdown(target: string, options: string[], data: ScanData | string | null): string {
// Use template literals for easier string construction and readability
let markdown = `## Scan Details - ${new Date().toISOString()}\\n\\n`;
markdown += `* **Target:** \\\`${target}\\\`\\n`; // Escaped backticks for markdown code format
markdown += `* **Options:** \\\`${options.length > 0 ? options.join(' ') : 'default'}\\\`\\n\\n`;
if (typeof data === 'string') {
// Handle error messages or simple string output
markdown += `**Result:** Error or simple message\\n\\\`\\\`\\\`\\n${data}\\n\\\`\\\`\\\`\\n`; // Escaped backticks for markdown code block
} else if (data && Object.keys(data).length > 0) {
markdown += `**Results Summary:**\\n\\n`;
for (const ip in data) {
// Ensure we are accessing valid Host data
if (!data[ip] || typeof data[ip] !== 'object') continue;
const hostData = data[ip] as Host; // Type assertion based on our d.ts
markdown += `### Host: ${hostData.hostname ? `${hostData.hostname} (${ip})` : ip}\\n\\n`;
if (hostData.mac) {
markdown += `* **MAC Address:** ${hostData.mac}\\n`;
}
if (hostData.osNmap) {
markdown += `* **OS Guess:** ${hostData.osNmap}\\n`;
}
if (hostData.ports && hostData.ports.length > 0) {
const openPorts = hostData.ports.filter(port => port.state === 'open');
if (openPorts.length > 0) {
markdown += `* **Open Ports:**\\n`;
markdown += ` | Port ID | Protocol | State | Service | Reason |\\n`;
markdown += ` |---------|----------|-------|---------|--------|\\n`;
openPorts.forEach(port => {
// Use optional chaining for potentially missing service details
markdown += ` | ${port.portId || 'N/A'} | ${port.protocol || 'N/A'} | ${port.state || 'N/A'} | ${port.service?.name || 'N/A'} | ${port.reason || 'N/A'} |\\n`;
});
} else {
markdown += `* No open ports found.\\n`;
}
} else {
markdown += `* Port scanning not performed or no ports reported.\\n`;
}
markdown += `\\n`; // Add space between hosts
}
} else {
markdown += `**Result:** No relevant data returned from scan.\\n`;
}
markdown += `\\n---\\n\\n`; // Separator
return markdown;
}
// Function to append formatted scan results to the markdown log file
export async function logScanResult(target: string, options: string[], results: ScanData | string | null): Promise<void> {
await ensureLogDirExists();
const formattedResult = formatScanDataToMarkdown(target, options, results);
try {
await fs.appendFile(LOG_FILE, formattedResult);
console.log(`Scan result logged to ${LOG_FILE}`);
} catch (error) {
console.error('Error writing scan result to log file:', error);
}
}
// Function to append general messages or errors to the log file
export async function logMessage(message: string): Promise<void> {
await ensureLogDirExists();
const timestamp = new Date().toISOString();
// Format general messages simply within a code block
const formattedMessage = `## Log Entry - ${timestamp}\\n\\n\\\`\\\`\\\`\\n${message}\\n\\\`\\\`\\\`\\n\\n---\\n\\n`;
try {
await fs.appendFile(LOG_FILE, formattedMessage);
console.log(`Message logged to ${LOG_FILE}`);
} catch (error) {
console.error('Error writing message to log file:', error);
}
}
// Function to retrieve the latest scan log entry for a specific target
export async function getLatestScanResultForTarget(target: string): Promise<string | null> {
try {
await ensureLogDirExists(); // Ensure directory/file potentially exists
const logContent = await fs.readFile(LOG_FILE, 'utf-8');
// Split the log into entries based on the separator
const entries = logContent.split('\n---\n');
// Search backwards for the latest entry matching the target
// This simple regex looks for the target string after "Target:"
// It assumes the target string doesn't contain regex special characters.
// A more robust approach might parse the markdown structure.
const targetRegex = new RegExp(`\* \*\*Target:\*\*\\\`\`${target}\\\`\``, 'i');
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i].trim();
if (entry.startsWith('## Scan Details') && targetRegex.test(entry)) {
// Return the relevant scan details block
return entry;
}
}
return null; // No entry found for the target
} catch (error: any) {
// If file doesn't exist (ENOENT) or other read errors, return null
if (error.code === 'ENOENT') {
return null;
}
console.error(`Error reading log file to retrieve scan for target ${target}:`, error);
return null; // Indicate failure to retrieve
}
}