@burtthecoder/mcp-virustotal
Version:
MCP server for VirusTotal API integration
155 lines (154 loc) • 7.02 kB
JavaScript
import { queryVirusTotal, queryVirusTotalWithRelationships } from '../utils/api.js';
import { formatDomainResults, formatRelationshipPage } from '../formatters/index.js';
import { logToFile } from '../utils/logging.js';
const DEFAULT_RELATIONSHIPS = [
'historical_whois',
'historical_ssl_certificates',
'resolutions',
'communicating_files',
'downloaded_files',
'referrer_files',
];
function formatDate(dateStr) {
try {
if (typeof dateStr === 'number') {
return new Date(dateStr * 1000).toLocaleDateString();
}
return new Date(dateStr).toLocaleDateString();
}
catch {
return 'Unknown';
}
}
function formatRelationshipData(relType, item) {
const attrs = item.attributes || {};
switch (relType) {
case 'resolutions':
return ` • IP: ${attrs.ip_address} (${attrs.date ? new Date(Number(attrs.date) * 1000).toLocaleDateString() : 'Unknown'})
Host: ${attrs.host_name || 'Unknown'}
Analysis Stats:
- IP: 🔴 ${attrs.ip_address_last_analysis_stats?.malicious || 0} malicious, ✅ ${attrs.ip_address_last_analysis_stats?.harmless || 0} harmless
- Host: 🔴 ${attrs.host_name_last_analysis_stats?.malicious || 0} malicious, ✅ ${attrs.host_name_last_analysis_stats?.harmless || 0} harmless`;
case 'communicating_files':
return ` • ${attrs.meaningful_name || item.id}
Type: ${attrs.type_description || attrs.type || 'Unknown'}
First Seen: ${attrs.first_submission_date ? new Date(attrs.first_submission_date * 1000).toLocaleDateString() : 'Unknown'}`;
case 'downloaded_files':
return ` • ${attrs.meaningful_name || item.id}
Type: ${attrs.type_description || attrs.type || 'Unknown'}
First Seen: ${attrs.first_submission_date ? new Date(attrs.first_submission_date * 1000).toLocaleDateString() : 'Unknown'}`;
case 'urls':
return ` • ${attrs.url || item.id}
Last Analysis: ${attrs.last_analysis_date ? new Date(attrs.last_analysis_date * 1000).toLocaleDateString() : 'Unknown'}
Reputation: ${attrs.reputation ?? 'Unknown'}`;
case 'historical_whois': {
const whoisMap = attrs.whois_map || {};
const whoisInfo = [];
if (whoisMap['Registrar'])
whoisInfo.push(`Registrar: ${whoisMap['Registrar']}`);
if (whoisMap['Creation Date'])
whoisInfo.push(`Created: ${formatDate(whoisMap['Creation Date'])}`);
if (whoisMap['Registry Expiry Date'])
whoisInfo.push(`Expires: ${formatDate(whoisMap['Registry Expiry Date'])}`);
if (whoisMap['Updated Date'])
whoisInfo.push(`Updated: ${formatDate(whoisMap['Updated Date'])}`);
if (whoisMap['Registrant Organization'])
whoisInfo.push(`Organization: ${whoisMap['Registrant Organization']}`);
if (attrs.registrar_name)
whoisInfo.push(`Registrar: ${attrs.registrar_name}`);
const lastUpdated = attrs.last_updated ? formatDate(attrs.last_updated) : 'Unknown';
return ` • WHOIS Record from ${lastUpdated}${whoisInfo.length ? '\n ' + whoisInfo.join('\n ') : ''}`;
}
case 'historical_ssl_certificates': {
const certInfo = [];
if (attrs.subject?.CN)
certInfo.push(`Subject: ${attrs.subject.CN}`);
if (attrs.issuer?.CN)
certInfo.push(`Issuer: ${attrs.issuer.CN}`);
if (attrs.validity?.not_before)
certInfo.push(`Valid From: ${formatDate(attrs.validity.not_before)}`);
if (attrs.validity?.not_after)
certInfo.push(`Valid Until: ${formatDate(attrs.validity.not_after)}`);
if (attrs.serial_number)
certInfo.push(`Serial: ${attrs.serial_number}`);
const altNames = attrs.extensions?.subject_alternative_name;
if (altNames && altNames.length)
certInfo.push(`Alt Names: ${altNames.join(', ')}`);
return ` • SSL Certificate${certInfo.length ? '\n ' + certInfo.join('\n ') : ''}`;
}
case 'referrer_files': {
const stats = attrs.last_analysis_stats || {};
const totalDetections = Object.values(stats).reduce((a, b) => a + b, 0);
return ` • ${attrs.meaningful_name || item.id}
Type: ${attrs.type_description || attrs.type || 'Unknown'}
Detection Ratio: ${attrs.last_analysis_stats
? `${attrs.last_analysis_stats.malicious}/${totalDetections}`
: 'Unknown'}`;
}
default:
if (attrs.hostname)
return ` • ${attrs.hostname}`;
if (attrs.ip_address)
return ` • ${attrs.ip_address}`;
if (attrs.url)
return ` • ${attrs.url}`;
if (attrs.value)
return ` • ${attrs.value}`;
return ` • ${item.id}`;
}
}
export async function handleGetDomainReport(args) {
const { domain, relationships = DEFAULT_RELATIONSHIPS } = args;
logToFile('Getting domain report with relationships...');
const report = (await queryVirusTotalWithRelationships(`/domains/${domain}`, relationships));
// Attach formattedOutput to each relationship item for the formatter.
const relationshipData = {};
for (const [relType, relData] of Object.entries(report.data?.relationships || {})) {
const typed = relData;
if (Array.isArray(typed.data)) {
relationshipData[relType] = {
data: typed.data.map((item) => ({
...item,
formattedOutput: formatRelationshipData(relType, item),
})),
meta: typed.meta,
};
}
else if (typed.data) {
relationshipData[relType] = {
data: {
...typed.data,
formattedOutput: formatRelationshipData(relType, typed.data),
},
meta: typed.meta,
};
}
}
const combinedData = {
...report.data,
relationships: relationshipData,
};
return {
content: [formatDomainResults(combinedData)],
};
}
export async function handleGetDomainRelationship(args) {
const { domain, relationship, limit, cursor } = args;
const params = { limit };
if (cursor)
params.cursor = cursor;
logToFile(`Fetching ${relationship} for domain: ${domain}`);
const result = await queryVirusTotal(`/domains/${domain}/${relationship}`, 'get', undefined, params);
return {
content: [
formatRelationshipPage({
entity: 'domain',
entityId: domain,
relationship,
data: result.data,
meta: result.meta,
renderItem: formatRelationshipData,
}),
],
};
}