@burtthecoder/mcp-virustotal
Version:
MCP server for VirusTotal API integration
214 lines (213 loc) ⢠12 kB
JavaScript
import { formatDateTime, formatDetectionResults } from './utils.js';
import { logToFile } from '../utils/logging.js';
export function formatRelationshipData(relType, item) {
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) => `${reg.key}: ${reg.value}`) || []),
...(attrs.registry_keys_deleted || [])
].filter(Boolean);
const network = attrs.ids_results?.map((result) => {
const ctx = result.alert_context || {};
return `${result.rule_msg} (${ctx.proto || '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;
const totalDetections = stats ?
Object.values(stats).reduce((a, b) => 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) {
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) => `⢠${formatCapabilityTag(tag)}`));
}
// Add tags if available
if (attributes?.tags?.length > 0) {
outputArray.push(``, `š·ļø Tags:`, ...attributes.tags.map((tag) => `⢠${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;
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;
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;
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;
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]) => `⢠${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) {
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) {
// Convert snake_case to Title Case and replace underscores with spaces
return tag
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}