/**
* Output formatting utilities
*/
import { ToolResult, ExecutionResult, ScanMetadata } from "../types.js";
import { SENSITIVE_PATTERNS } from "../constants.js";
/**
* Sanitize output by redacting sensitive information
*/
export function sanitizeOutput(output: string): string {
let sanitized = output;
for (const pattern of SENSITIVE_PATTERNS) {
sanitized = sanitized.replace(pattern, "[REDACTED]");
}
return sanitized;
}
/**
* Format execution result as tool result
*/
export function formatExecutionResult(
result: ExecutionResult,
includeMetadata = true
): ToolResult {
if (!result.success) {
return {
isError: true,
content: [
{
type: "text",
text: formatError(result),
},
],
};
}
const output = sanitizeOutput(result.stdout);
if (includeMetadata) {
return {
content: [
{
type: "text",
text: `${output}\n\n---\nCompleted in ${result.duration}ms`,
},
],
};
}
return {
content: [
{
type: "text",
text: output,
},
],
};
}
/**
* Format error message
*/
export function formatError(result: ExecutionResult): string {
const parts: string[] = [];
parts.push(`❌ Command failed: ${result.command}`);
if (result.timedOut) {
parts.push(`\n⏱️ Timeout: Command exceeded time limit (${result.duration}ms)`);
} else {
parts.push(`\n🔢 Exit code: ${result.exitCode}`);
}
if (result.stderr) {
parts.push(`\n\n📋 Error output:\n${sanitizeOutput(result.stderr)}`);
}
if (result.stdout) {
parts.push(`\n\n📄 Standard output:\n${sanitizeOutput(result.stdout)}`);
}
return parts.join("");
}
/**
* Format as JSON
*/
export function formatJSON(data: any): ToolResult {
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
}
/**
* Format as markdown
*/
export function formatMarkdown(title: string, content: string): ToolResult {
return {
content: [
{
type: "text",
text: `# ${title}\n\n${content}`,
},
],
};
}
/**
* Format scan results with metadata
*/
export function formatScanResult(
metadata: ScanMetadata,
output: string,
format: "text" | "json" | "markdown" = "text"
): ToolResult {
const sanitized = sanitizeOutput(output);
if (format === "json") {
return formatJSON({
metadata,
results: sanitized,
});
}
if (format === "markdown") {
const md = `# ${metadata.toolName} Scan Results
**Target:** ${metadata.target}
**Started:** ${metadata.startTime}
**Completed:** ${metadata.endTime}
**Duration:** ${metadata.duration}ms
${metadata.toolVersion ? `**Tool Version:** ${metadata.toolVersion}` : ""}
## Results
\`\`\`
${sanitized}
\`\`\`
`;
return {
content: [
{
type: "text",
text: md,
},
],
};
}
// Text format
return {
content: [
{
type: "text",
text: `${metadata.toolName} - ${metadata.target}\n${"=".repeat(50)}\n\n${sanitized}\n\nCompleted in ${metadata.duration}ms`,
},
],
};
}
/**
* Format table data
*/
export function formatTable(headers: string[], rows: string[][]): string {
if (rows.length === 0) {
return "No results found.";
}
// Calculate column widths
const widths = headers.map((h, i) => {
const maxRowWidth = Math.max(...rows.map((r) => (r[i] || "").length));
return Math.max(h.length, maxRowWidth);
});
// Format header
const headerRow = headers
.map((h, i) => h.padEnd(widths[i] || 0))
.join(" | ");
const separator = widths.map((w) => "-".repeat(w)).join("-+-");
// Format rows
const dataRows = rows
.map((row) => row.map((cell, i) => (cell || "").padEnd(widths[i] || 0)).join(" | "))
.join("\n");
return `${headerRow}\n${separator}\n${dataRows}`;
}
/**
* Format list with bullet points
*/
export function formatList(items: string[], ordered = false): string {
if (items.length === 0) {
return "No items.";
}
return items
.map((item, i) => {
const prefix = ordered ? `${i + 1}.` : "•";
return `${prefix} ${item}`;
})
.join("\n");
}
/**
* Format port scan results
*/
export function formatPortScanResults(ports: Array<{
port: number;
state: string;
service?: string;
version?: string;
}>): string {
if (ports.length === 0) {
return "No open ports found.";
}
const headers = ["Port", "State", "Service", "Version"];
const rows = ports.map((p) => [
String(p.port),
p.state,
p.service || "unknown",
p.version || "-",
]);
return formatTable(headers, rows);
}
/**
* Format vulnerability findings
*/
export function formatVulnerabilities(vulns: Array<{
id: string;
severity: string;
title: string;
description?: string;
}>): string {
if (vulns.length === 0) {
return "✅ No vulnerabilities found.";
}
const severityEmoji = {
critical: "🔴",
high: "🟠",
medium: "🟡",
low: "🟢",
info: "ℹ️",
};
let output = `Found ${vulns.length} vulnerabilities:\n\n`;
for (const vuln of vulns) {
const emoji = severityEmoji[vuln.severity.toLowerCase() as keyof typeof severityEmoji] || "⚠️";
output += `${emoji} **${vuln.severity.toUpperCase()}** - ${vuln.title}\n`;
output += ` ID: ${vuln.id}\n`;
if (vuln.description) {
output += ` ${vuln.description}\n`;
}
output += "\n";
}
return output;
}
/**
* Format progress update
*/
export function formatProgress(current: number, total: number, message?: string): string {
const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
const bar = "█".repeat(Math.floor(percentage / 5)) + "░".repeat(20 - Math.floor(percentage / 5));
let output = `[${bar}] ${percentage}% (${current}/${total})`;
if (message) {
output += ` - ${message}`;
}
return output;
}
/**
* Format success message
*/
export function formatSuccess(message: string): ToolResult {
return {
content: [
{
type: "text",
text: `✅ ${message}`,
},
],
};
}
/**
* Format error message
*/
export function formatErrorMessage(message: string, details?: string): ToolResult {
let text = `❌ ${message}`;
if (details) {
text += `\n\n${details}`;
}
return {
isError: true,
content: [
{
type: "text",
text,
},
],
};
}
/**
* Format warning message
*/
export function formatWarning(message: string): ToolResult {
return {
content: [
{
type: "text",
text: `⚠️ ${message}`,
},
],
};
}
/**
* Format command help
*/
export function formatHelp(
toolName: string,
description: string,
examples: Array<{ description: string; command: string }>
): string {
let output = `# ${toolName}\n\n${description}\n\n## Examples\n\n`;
for (const example of examples) {
output += `**${example.description}:**\n\`\`\`\n${example.command}\n\`\`\`\n\n`;
}
return output;
}
/**
* Truncate long output
*/
export function truncateOutput(output: string, maxLength = 5000): string {
if (output.length <= maxLength) {
return output;
}
const truncated = output.substring(0, maxLength);
return `${truncated}\n\n... [Output truncated - ${output.length - maxLength} more characters]`;
}
/**
* Parse nmap XML output to structured format
*/
export function parseNmapXML(xml: string): any {
// Simplified parser - in production, use a proper XML parser
// This is a placeholder that returns the raw XML
return {
raw: xml,
note: "XML parsing not yet implemented - showing raw output",
};
}
/**
* Parse CSV output
*/
export function parseCSV(csv: string): string[][] {
const lines = csv.trim().split("\n");
return lines.map((line) => line.split(",").map((cell) => cell.trim()));
}
/**
* Format duration in human-readable format
*/
export function formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(2)}s`;
} else if (ms < 3600000) {
return `${(ms / 60000).toFixed(2)}m`;
} else {
return `${(ms / 3600000).toFixed(2)}h`;
}
}
/**
* Format file size
*/
export function formatFileSize(bytes: number): string {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}