VirusTotal MCP Server
by BurtTheCoder
- mcp-virustotal
- src
- formatters
// src/formatters/url.ts
import { FormattedResult } from './types.js';
import { formatDateTime, formatDetectionResults } from './utils.js';
import { logToFile } from '../utils/logging.js';
import { RelationshipData } from '../types/virustotal.js';
interface TrackerInstance {
id: string;
timestamp: number;
url: string;
}
export interface UrlAttributes {
url?: string;
last_final_url?: string;
title?: string;
categories?: Record<string, string>;
first_submission_date?: number;
last_analysis_date?: number;
last_modification_date?: number;
times_submitted?: number;
last_http_response_code?: number;
last_http_response_content_length?: number;
last_http_response_content_sha256?: string;
last_http_response_cookies?: Record<string, string>;
last_http_response_headers?: Record<string, string>;
reputation?: number;
html_meta?: Record<string, string[]>;
redirection_chain?: string[];
outgoing_links?: string[];
trackers?: Record<string, TrackerInstance[]>;
targeted_brand?: Record<string, string>;
tags?: string[];
total_votes?: {
harmless: number;
malicious: number;
};
favicon?: {
dhash: string;
raw_md5: string;
};
last_analysis_stats?: {
harmless: number;
malicious: number;
suspicious: number;
timeout: number;
undetected: number;
};
last_analysis_results?: Record<string, {
category: string;
engine_name: string;
method: string;
result: string | null;
}>;
}
export interface UrlData {
id?: string;
url?: string;
attributes?: UrlAttributes;
scan_id?: string;
scan_date?: string;
relationships?: Record<string, RelationshipData>;
}
function formatRelationshipData(relType: string, item: any): string {
const attrs = item.attributes || {};
switch (relType) {
case 'communicating_files':
case 'downloaded_files':
return ` • ${attrs.meaningful_name || item.id}
Type: ${attrs.type_description || attrs.type || 'Unknown'}
First Seen: ${attrs.first_submission_date ? formatDateTime(attrs.first_submission_date) : 'Unknown'}`;
case 'contacted_domains':
return ` • ${attrs.id || item.id}
Last DNS Resolution: ${attrs.last_dns_records_date ? formatDateTime(attrs.last_dns_records_date) : 'Unknown'}
Categories: ${Object.entries(attrs.categories || {}).map(([k, v]) => `${k}: ${v}`).join(', ') || 'None'}`;
case 'contacted_ips':
return ` • ${attrs.ip_address || item.id}
Country: ${attrs.country || 'Unknown'}
AS Owner: ${attrs.as_owner || 'Unknown'}
Last Analysis Stats: ${attrs.last_analysis_stats ?
`🔴 ${attrs.last_analysis_stats.malicious} malicious, ✅ ${attrs.last_analysis_stats.harmless} harmless` :
'Unknown'}`;
case 'redirects_to':
case 'redirecting_urls':
return ` • ${attrs.url || item.id}
Last Analysis: ${attrs.last_analysis_date ? formatDateTime(attrs.last_analysis_date) : 'Unknown'}
Reputation: ${attrs.reputation ?? 'Unknown'}`;
case 'related_threat_actors':
return ` • ${attrs.name || item.id}
Description: ${attrs.description || 'No description available'}`;
default:
return ` • ${item.id}`;
}
}
export function formatUrlScanResults(data: UrlData): FormattedResult {
try {
const attributes = data?.attributes || {};
const stats = attributes?.last_analysis_stats || {};
const votes = attributes?.total_votes || { harmless: 0, malicious: 0 };
const tags = attributes?.tags || [];
const redirectionChain = attributes?.redirection_chain || [];
const outgoingLinks = attributes?.outgoing_links || [];
let outputArray = [
`🔍 URL Analysis Results`,
``,
`🌐 URL Information:`,
`• URL: ${attributes?.url || data?.url || data?.id || 'Unknown URL'}`,
attributes?.last_final_url && attributes.last_final_url !== attributes.url ?
`• Final URL: ${attributes.last_final_url}` : null,
attributes?.title ? `• Page Title: ${attributes.title}` : null,
`• First Seen: ${attributes.first_submission_date ? formatDateTime(attributes.first_submission_date) : 'N/A'}`,
`• Last Analyzed: ${attributes.last_analysis_date ? formatDateTime(attributes.last_analysis_date) : 'N/A'}`,
`• Times Submitted: ${attributes?.times_submitted || 0}`,
``,
`📊 Analysis Statistics:`,
formatDetectionResults(stats),
].filter(Boolean);
// Add reputation and votes
const reputation = attributes?.reputation ?? 'N/A';
outputArray.push(
``,
`👥 Community Feedback:`,
`• Reputation Score: ${reputation}`,
`• Harmless Votes: ${votes.harmless}`,
`• Malicious Votes: ${votes.malicious}`
);
// Add HTTP response details
if (attributes?.last_http_response_code) {
outputArray.push(
``,
`🌐 HTTP Response:`,
`• Status Code: ${attributes.last_http_response_code}`,
`• Content Length: ${formatSize(attributes.last_http_response_content_length || 0)}`,
attributes.last_http_response_content_sha256 ?
`• Content SHA-256: ${attributes.last_http_response_content_sha256}` : null
);
}
// Add categories if available
if (attributes?.categories && Object.keys(attributes.categories).length > 0) {
outputArray.push(
``,
`🏷️ Categories:`,
...Object.entries(attributes.categories).map(([service, category]) =>
`• ${service}: ${category}`
)
);
}
// Add redirection chain if available
if (redirectionChain.length > 0) {
outputArray.push(
``,
`↪️ Redirection Chain:`,
...redirectionChain.map((url: string, index: number) =>
`${index + 1}. ${url}`
)
);
}
// Add outgoing links if available
if (outgoingLinks.length > 0) {
outputArray.push(
``,
`🔗 Outgoing Links:`,
...outgoingLinks.slice(0, 5).map((url: string) => `• ${url}`),
outgoingLinks.length > 5 ?
`... and ${outgoingLinks.length - 5} more` : null
);
}
// Add trackers if available
if (attributes?.trackers && Object.keys(attributes.trackers).length > 0) {
outputArray.push(
``,
`📡 Trackers:`
);
for (const [tracker, instances] of Object.entries(attributes.trackers)) {
if (Array.isArray(instances)) {
outputArray.push(
`${tracker}:`,
...instances.map((instance: TrackerInstance) =>
`• ID: ${instance.id}${instance.url ? `\n URL: ${instance.url}` : ''}`
)
);
}
}
}
// Add targeted brand if available
if (attributes?.targeted_brand && Object.keys(attributes.targeted_brand).length > 0) {
outputArray.push(
``,
`🎯 Targeted Brands:`,
...Object.entries(attributes.targeted_brand).map(([source, brand]) =>
`• ${source}: ${brand}`
)
);
}
// Add meta information if available
if (attributes?.html_meta && Object.keys(attributes.html_meta).length > 0) {
const relevantMeta = ['description', 'keywords', 'author'];
const metaEntries = Object.entries(attributes.html_meta)
.filter(([key]) => relevantMeta.includes(key));
if (metaEntries.length > 0) {
outputArray.push(
``,
`📝 Meta Information:`
);
for (const [key, values] of metaEntries) {
if (Array.isArray(values) && values.length > 0) {
outputArray.push(`• ${key}: ${values[0]}`);
}
}
}
}
// Add favicon information if available
if (attributes?.favicon) {
outputArray.push(
``,
`🖼️ Favicon:`,
`• Hash: ${attributes.favicon.dhash}`,
`• MD5: ${attributes.favicon.raw_md5}`
);
}
// Add tags if available
if (tags.length > 0) {
outputArray.push(
``,
`🏷️ Tags:`,
...tags.map((tag: string) => `• ${tag}`)
);
}
// Add HTTP response headers if available
if (attributes?.last_http_response_headers &&
Object.keys(attributes.last_http_response_headers).length > 0) {
const importantHeaders = ['server', 'content-type', 'x-powered-by', 'x-frame-options', 'x-xss-protection'];
const relevantHeaders = Object.entries(attributes.last_http_response_headers)
.filter(([key]) => importantHeaders.includes(key.toLowerCase()));
if (relevantHeaders.length > 0) {
outputArray.push(
``,
`📋 Important HTTP Headers:`
);
for (const [key, value] of relevantHeaders) {
outputArray.push(`• ${key}: ${value}`);
}
}
}
// Add HTTP response cookies if available
if (attributes?.last_http_response_cookies &&
Object.keys(attributes.last_http_response_cookies).length > 0) {
outputArray.push(
``,
`🍪 Cookies:`,
...Object.entries(attributes.last_http_response_cookies)
.map(([name, value]) => `• ${name}: ${value}`)
);
}
// Format relationships if available
if (data.relationships) {
outputArray.push('\n🔗 Relationships:');
for (const [relType, relData] of Object.entries(data.relationships)) {
const count = relData.meta?.count || (Array.isArray(relData.data) ? relData.data.length : 1);
outputArray.push(`\n${relType} (${count} items):`);
if (Array.isArray(relData.data)) {
relData.data.forEach(item => {
outputArray.push(formatRelationshipData(relType, item));
});
} else if (relData.data) {
outputArray.push(formatRelationshipData(relType, relData.data));
}
}
}
return {
type: "text",
text: outputArray.filter(Boolean).join('\n')
};
} catch (error) {
logToFile(`Error formatting URL scan results: ${error}`);
return {
type: "text",
text: "Error formatting URL scan results"
};
}
}
function formatSize(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]}`;
}