VirusTotal MCP Server
by BurtTheCoder
- mcp-virustotal
- src
- formatters
// src/formatters/file.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 SandboxVerdict {
category: string;
confidence: number;
malware_classification?: string[];
malware_names?: string[];
sandbox_name: string;
}
interface CrowdsourcedIdsResult {
alert_severity: string;
rule_category: string;
rule_id: string;
rule_msg: string;
rule_source: string;
alert_context?: Array<{
proto?: string;
src_ip?: string;
src_port?: number;
dest_ip?: string; // Add this
dest_port?: number; // Add this
hostname?: string; // Add this
url?: string; // Add this
}>;
}
interface YaraResult {
description: string;
match_in_subfile: boolean;
rule_name: string;
ruleset_id: string;
ruleset_name: string;
source: string;
}
interface SigmaResult {
rule_title: string;
rule_source: string;
rule_level: string;
rule_description: string;
rule_author: string;
rule_id: string;
match_context?: Array<{
values: Record<string, string>;
}>;
}
interface FileData {
attributes?: any;
relationships?: Record<string, RelationshipData>;
}
interface AnalysisStats {
harmless: number;
malicious: number;
suspicious: number;
undetected: number;
timeout: number;
}
function formatRelationshipData(relType: string, item: any): string {
const attrs = item.attributes || {};
switch (relType) {
case 'behaviours':
const activities = [
...(attrs.processes_created || []),
...(attrs.command_executions || []),
...(attrs.activities_started || [])
].filter(Boolean);
const files = [
...(attrs.files_opened || []),
...(attrs.files_written || [])
].filter(Boolean);
const registry = [
...(attrs.registry_keys_opened || []),
...(attrs.registry_keys_set?.map((reg: any) => `${reg.key}: ${reg.value}`) || []),
...(attrs.registry_keys_deleted || [])
].filter(Boolean);
const network = attrs.ids_results?.map((result: any) => {
const ctx = result.alert_context || {};
return `${result.rule_msg} (${ctx.protocol || 'unknown'} ${ctx.src_ip || ''}:${ctx.src_port || ''} -> ${ctx.dest_ip || ''}:${ctx.dest_port || ''})`;
}) || [];
return ` • Sandbox: ${attrs.sandbox_name || 'Unknown'}
Activities (${activities.length}):${activities.length ? '\n - ' + activities.slice(0, 5).join('\n - ') + (activities.length > 5 ? '\n ... and more' : '') : ' None'}
Files (${files.length}):${files.length ? '\n - ' + files.slice(0, 5).join('\n - ') + (files.length > 5 ? '\n ... and more' : '') : ' None'}
Registry (${registry.length}):${registry.length ? '\n - ' + registry.slice(0, 5).join('\n - ') + (registry.length > 5 ? '\n ... and more' : '') : ' None'}
Network (${network.length}):${network.length ? '\n - ' + network.slice(0, 5).join('\n - ') + (network.length > 5 ? '\n ... and more' : '') : ' None'}
Verdicts: ${attrs.verdicts?.join(', ') || 'None'}`;
case 'contacted_domains':
case 'embedded_domains':
case 'itw_domains':
return ` • ${attrs.id || item.id}
Categories: ${Object.entries(attrs.categories || {}).map(([k, v]) => `${k}: ${v}`).join(', ') || 'None'}
Last Analysis Stats: ${attrs.last_analysis_stats ?
`🔴 ${attrs.last_analysis_stats.malicious} malicious, ✅ ${attrs.last_analysis_stats.harmless} harmless` :
'Unknown'}`;
case 'contacted_ips':
case 'embedded_ips':
case 'itw_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 'contacted_urls':
case 'embedded_urls':
case 'itw_urls':
return ` • ${attrs.url || item.id}
Last Analysis: ${attrs.last_analysis_date ? formatDateTime(attrs.last_analysis_date) : 'Unknown'}
Reputation: ${attrs.reputation ?? 'Unknown'}`;
case 'dropped_files':
return ` • ${attrs.meaningful_name || attrs.names?.[0] || item.id}
Type: ${attrs.type_description || attrs.type || 'Unknown'}
Size: ${attrs.size ? formatFileSize(attrs.size) : 'Unknown'}
First Seen: ${attrs.first_submission_date ? formatDateTime(attrs.first_submission_date) : 'Unknown'}
Detection Stats: ${attrs.last_analysis_stats ?
`🔴 ${attrs.last_analysis_stats.malicious} malicious, ✅ ${attrs.last_analysis_stats.harmless} harmless` :
'Unknown'}`;
case 'similar_files':
return ` • ${attrs.meaningful_name || item.id}
Type: ${attrs.type_description || attrs.type || 'Unknown'}
Size: ${attrs.size ? formatFileSize(attrs.size) : 'Unknown'}
First Seen: ${attrs.first_submission_date ? formatDateTime(attrs.first_submission_date) : 'Unknown'}`;
case 'execution_parents':
const stats = attrs.last_analysis_stats as AnalysisStats;
const totalDetections = stats ?
Object.values(stats).reduce((a: number, b: number) => a + b, 0) : 0;
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'}
Detection Ratio: ${stats ? `${stats.malicious}/${totalDetections}` : 'Unknown'}`;
case 'related_threat_actors':
return ` • ${attrs.name || item.id}
Description: ${attrs.description || 'No description available'}`;
default:
return ` • ${item.id}`;
}
}
export function formatFileResults(data: FileData): FormattedResult {
try {
const attributes = data?.attributes || {};
const stats = attributes?.last_analysis_stats || {};
const results = attributes?.last_analysis_results || {};
const sandboxResults = attributes?.sandbox_verdicts || {};
const sigmaResults = attributes?.sigma_analysis_results || [];
const sigmaStats = attributes?.sigma_analysis_stats || {};
const crowdsourcedIds = attributes?.crowdsourced_ids_results || [];
const crowdsourcedIdsStats = attributes?.crowdsourced_ids_stats || {};
const yaraResults = attributes?.crowdsourced_yara_results || [];
let outputArray = [
`📁 File Analysis Results`,
`🔑 Hashes:`,
`• SHA-256: ${attributes?.sha256 || 'Unknown'}`,
`• SHA-1: ${attributes?.sha1 || 'Unknown'}`,
`• MD5: ${attributes?.md5 || 'Unknown'}`,
attributes?.vhash ? `• VHash: ${attributes.vhash}` : null,
``,
`📄 File Information:`,
`• Name: ${attributes?.meaningful_name || attributes?.names?.[0] || 'Unknown'}`,
`• Type: ${attributes?.type_description || 'Unknown'}`,
`• Size: ${attributes?.size ? formatFileSize(attributes.size) : 'Unknown'}`,
`• First Seen: ${formatDateTime(attributes?.first_submission_date)}`,
`• Last Modified: ${formatDateTime(attributes?.last_modification_date)}`,
`• Times Submitted: ${attributes?.times_submitted || 0}`,
`• Unique Sources: ${attributes?.unique_sources || 0}`,
``,
`📊 Analysis Statistics:`,
formatDetectionResults(stats),
].filter(Boolean);
// Add reputation and votes
const reputation = attributes?.reputation ?? 'N/A';
const votes = attributes?.total_votes || {};
outputArray.push(
``,
`👥 Community Feedback:`,
`• Reputation Score: ${reputation}`,
`• Harmless Votes: ${votes.harmless || 0}`,
`• Malicious Votes: ${votes.malicious || 0}`
);
// Add capabilities if available
if (attributes?.capabilities_tags?.length > 0) {
outputArray.push(
``,
`⚡ Capabilities:`,
...attributes.capabilities_tags.map((tag: string) => `• ${formatCapabilityTag(tag)}`)
);
}
// Add tags if available
if (attributes?.tags?.length > 0) {
outputArray.push(
``,
`🏷️ Tags:`,
...attributes.tags.map((tag: string) => `• ${tag}`)
);
}
// Add sandbox verdicts if available
if (Object.keys(sandboxResults).length > 0) {
outputArray.push(
``,
`🔬 Sandbox Analysis Results:`
);
for (const [sandbox, verdict] of Object.entries(sandboxResults)) {
const v = verdict as SandboxVerdict;
outputArray.push(
`${sandbox}:`,
`• Category: ${v.category}`,
`• Confidence: ${v.confidence}%`,
...(v.malware_classification ? [`• Classification: ${v.malware_classification.join(', ')}`] : []),
...(v.malware_names ? [`• Identified as: ${v.malware_names.join(', ')}`] : []),
``
);
}
}
// Add Sigma analysis results if available
if (sigmaResults.length > 0 || Object.keys(sigmaStats).length > 0) {
outputArray.push(
``,
`🎯 Sigma Analysis:`,
`Statistics:`,
`• Critical: ${sigmaStats.critical || 0}`,
`• High: ${sigmaStats.high || 0}`,
`• Medium: ${sigmaStats.medium || 0}`,
`• Low: ${sigmaStats.low || 0}`,
``
);
if (sigmaResults.length > 0) {
outputArray.push(`Detected Rules:`);
for (const result of sigmaResults) {
const r = result as SigmaResult;
outputArray.push(
`${r.rule_title}:`,
`• Level: ${r.rule_level}`,
`• Source: ${r.rule_source}`,
`• Description: ${r.rule_description}`,
`• Author: ${r.rule_author}`,
``
);
}
}
}
// Add crowdsourced IDS results if available
if (crowdsourcedIds.length > 0) {
outputArray.push(
``,
`🛡️ Intrusion Detection Results:`,
`Statistics:`,
`• High: ${crowdsourcedIdsStats.high || 0}`,
`• Medium: ${crowdsourcedIdsStats.medium || 0}`,
`• Low: ${crowdsourcedIdsStats.low || 0}`,
`• Info: ${crowdsourcedIdsStats.info || 0}`,
``
);
outputArray.push(`Alerts:`);
for (const alert of crowdsourcedIds) {
const a = alert as CrowdsourcedIdsResult;
outputArray.push(
`• ${a.rule_msg}`,
` - Severity: ${a.alert_severity}`,
` - Category: ${a.rule_category}`,
` - Source: ${a.rule_source}`,
``
);
}
}
// Add YARA results if available
if (yaraResults.length > 0) {
outputArray.push(
``,
`🔍 YARA Detections:`
);
for (const yara of yaraResults) {
const y = yara as YaraResult;
outputArray.push(
`• Rule: ${y.rule_name}`,
` - Description: ${y.description}`,
` - Ruleset: ${y.ruleset_name}`,
` - Source: ${y.source}`,
``
);
}
}
// Add popular engine results
const popularEngines = Object.entries(results)
.filter(([engine]) => ['Microsoft', 'Kaspersky', 'Symantec', 'McAfee'].includes(engine));
if (popularEngines.length > 0) {
outputArray.push(
``,
`🔰 Popular Engines Results:`,
...popularEngines.map(([engine, result]: [string, any]) =>
`• ${engine}: ${result?.result || 'Clean'} ${
result?.category === 'malicious' ? '🔴' :
result?.category === 'suspicious' ? '⚠️' : '✅'
}`
)
);
}
// 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 file results: ${error}`);
return {
type: "text",
text: "Error formatting file results"
};
}
}
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]}`;
}
function formatCapabilityTag(tag: string): string {
// Convert snake_case to Title Case and replace underscores with spaces
return tag
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}