UNPKG

@burtthecoder/mcp-virustotal

Version:
128 lines (127 loc) 5.57 kB
import axios from 'axios'; import { logToFile } from './logging.js'; export class VirusTotalApiError extends Error { constructor(message, status, code) { super(message); this.status = status; this.code = code; this.name = 'VirusTotalApiError'; } } let axiosInstance = null; export function initVirusTotalClient() { const apiKey = process.env.VIRUSTOTAL_API_KEY; if (!apiKey) { throw new Error('VIRUSTOTAL_API_KEY environment variable is required'); } axiosInstance = axios.create({ baseURL: 'https://www.virustotal.com/api/v3', headers: { 'x-apikey': apiKey }, }); } function getClient() { if (!axiosInstance) initVirusTotalClient(); return axiosInstance; } export async function queryVirusTotal(endpoint, method = 'get', data, params) { if (!endpoint) throw new Error('Endpoint is required'); const client = getClient(); try { logToFile(`${method.toUpperCase()} ${endpoint}`); if (params) logToFile(`Request params: ${Object.keys(params).join(', ')}`); const response = method === 'get' ? await client.get(endpoint, { params }) : await client.post(endpoint, data, { params }); logToFile(`Response status: ${response.status}`); return response.data; } catch (error) { if (axios.isAxiosError(error)) { const axiosError = error; const status = axiosError.response?.status; const code = axiosError.response?.data?.error?.code; const message = axiosError.response?.data?.error?.message || axiosError.message; logToFile(`API Error: ${status} - ${message}`); throw new VirusTotalApiError(`VirusTotal API error: ${message}`, status, code); } throw error; } } export function encodeUrlForVt(url) { return Buffer.from(url).toString('base64url'); } // Canonical relationship lists per VirusTotal API v3 docs // (https://virustotal.readme.io/llms.txt — verified May 2026). // Single source of truth: schemas/index.ts derives Zod enums from this. export const RELATIONSHIPS = { url: [ 'analyses', 'collections', 'comments', 'communicating_files', 'contacted_domains', 'contacted_ips', 'downloaded_files', 'embedded_js_files', 'graphs', 'last_serving_ip_address', 'network_location', 'referrer_files', 'referrer_urls', 'redirecting_urls', 'redirects_to', 'related_comments', 'related_references', 'related_threat_actors', 'submissions', 'urls_related_by_tracker_id', 'user_votes', 'votes', ], file: [ 'analyses', 'behaviours', 'bundled_files', 'carbonblack_children', 'carbonblack_parents', 'ciphered_bundled_files', 'ciphered_parents', 'collections', 'comments', 'compressed_parents', 'contacted_domains', 'contacted_ips', 'contacted_urls', 'dropped_files', 'email_attachments', 'email_parents', 'embedded_domains', 'embedded_ips', 'embedded_urls', 'execution_parents', 'graphs', 'itw_domains', 'itw_ips', 'itw_urls', 'memory_pattern_domains', 'memory_pattern_ips', 'memory_pattern_urls', 'overlay_children', 'overlay_parents', 'pcap_children', 'pcap_parents', 'pe_resource_children', 'pe_resource_parents', 'related_references', 'related_threat_actors', 'similar_files', 'submissions', 'screenshots', 'urls_for_embedded_js', 'votes', ], ip: [ 'collections', 'comments', 'communicating_files', 'downloaded_files', 'graphs', 'historical_ssl_certificates', 'historical_whois', 'related_comments', 'related_references', 'related_threat_actors', 'referrer_files', 'resolutions', 'urls', 'user_votes', 'votes', ], domain: [ 'caa_records', 'cname_records', 'collections', 'comments', 'communicating_files', 'downloaded_files', 'graphs', 'historical_ssl_certificates', 'historical_whois', 'immediate_parent', 'mx_records', 'ns_records', 'parent', 'referrer_files', 'related_comments', 'related_references', 'related_threat_actors', 'resolutions', 'soa_records', 'siblings', 'subdomains', 'urls', 'user_votes', 'votes', ], collection: [ 'autogenerated_graphs', 'comments', 'domains', 'files', 'ip_addresses', 'owner', 'references', 'related_collections', 'related_references', 'threat_actors', 'urls', ], }; // VT caps the relationships you can request in a single call; batch to be safe. const RELATIONSHIPS_PER_REQUEST = 8; // Fetch an object + a set of relationship summaries in as few calls as // possible by using VT's `?relationships=a,b,c` query, batched. export async function queryVirusTotalWithRelationships(baseEndpoint, relationships) { if (relationships.length === 0) { return queryVirusTotal(baseEndpoint); } const batches = []; for (let i = 0; i < relationships.length; i += RELATIONSHIPS_PER_REQUEST) { batches.push([...relationships.slice(i, i + RELATIONSHIPS_PER_REQUEST)]); } const responses = await Promise.all(batches.map((batch) => queryVirusTotal(baseEndpoint, 'get', undefined, { relationships: batch.join(','), }))); const base = responses[0]; const mergedRelationships = responses.reduce((acc, r) => ({ ...acc, ...(r?.data?.relationships || {}) }), {}); return { ...base, data: { ...base.data, relationships: mergedRelationships, }, }; }