UNPKG

trojanhorse-js

Version:

A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.

755 lines (665 loc) 22.3 kB
/** * VirusTotal Feed Integration * * Integrates with VirusTotal API for file hash and URL analysis * Supports both free and premium API tiers */ import axios from 'axios'; import { FeedConfiguration, ThreatFeedResult, ThreatIndicator } from '../types'; interface VirusTotalConfig { apiKey: string; tier?: 'free' | 'premium'; baseUrl?: string; timeout?: number; retries?: number; } interface VirusTotalFileResponse { data: { id: string; type: string; attributes: { sha256: string; md5: string; sha1: string; meaningful_name: string; size: number; last_analysis_date: number; last_analysis_stats: { harmless: number; type_unsupported: number; suspicious: number; confirmed_timeout: number; timeout: number; failure: number; malicious: number; undetected: number; }; last_analysis_results: Record<string, { category: string; engine_name: string; engine_version: string; result: string; method: string; engine_update: string; }>; reputation: number; popular_threat_classification?: { suggested_threat_label: string; popular_threat_category: Array<{ count: number; value: string; }>; }; }; }; } interface VirusTotalUrlResponse { data: { id: string; type: string; attributes: { url: string; last_analysis_date: number; last_analysis_stats: { harmless: number; malicious: number; suspicious: number; undetected: number; timeout: number; }; last_analysis_results: Record<string, { category: string; result: string; method: string; engine_name: string; }>; reputation: number; total_votes: { harmless: number; malicious: number; }; }; }; } interface VirusTotalDomainResponse { data: { id: string; type: string; attributes: { last_analysis_date: number; last_analysis_stats: { harmless: number; malicious: number; suspicious: number; undetected: number; timeout: number; }; reputation: number; whois: string; categories: Record<string, string>; last_dns_records: Array<{ type: string; value: string; ttl: number; }>; }; }; } export class VirusTotalFeed { private config: FeedConfiguration; private vtConfig: VirusTotalConfig; private requestCount: number = 0; private lastRequest: number = 0; private errorCount: number = 0; private cache: Map<string, { data: any; expires: number }> = new Map(); constructor(vtConfig: VirusTotalConfig) { this.vtConfig = { tier: 'free', baseUrl: 'https://www.virustotal.com/api/v3', timeout: 30000, retries: 3, ...vtConfig }; this.config = { name: 'VirusTotal', type: 'api', endpoint: this.vtConfig.baseUrl!, authentication: { type: 'api_key', header: 'x-apikey', required: true }, rateLimit: { requestsPerHour: this.vtConfig.tier === 'premium' ? 5000 : 1000, burstLimit: this.vtConfig.tier === 'premium' ? 300 : 4 }, enabled: true, priority: 'high', sslPinning: true, apiKey: vtConfig.apiKey, timeout: this.vtConfig.timeout || 30000, retries: this.vtConfig.retries || 3 }; this.validateApiKey(); } private validateApiKey(): void { if (!this.vtConfig.apiKey || this.vtConfig.apiKey.length < 64) { throw new Error('Invalid VirusTotal API key. Must be at least 64 characters.'); } } private async makeRequest(endpoint: string, retryCount = 0): Promise<any> { try { await this.enforceRateLimit(); const response = await axios.get(`${this.vtConfig.baseUrl}${endpoint}`, { headers: { 'x-apikey': this.vtConfig.apiKey, 'User-Agent': 'TrojanHorse.js/1.0.1', 'Accept': 'application/json' }, timeout: this.vtConfig.timeout || 30000 }); this.requestCount++; this.lastRequest = Date.now(); return response.data; } catch (error: any) { this.errorCount++; if (error.response?.status === 429 && retryCount < this.vtConfig.retries!) { const retryAfter = parseInt(error.response.headers['retry-after'] || '60'); console.warn(`[VirusTotal] Rate limited, retrying after ${retryAfter}s`); await this.wait(retryAfter * 1000); return this.makeRequest(endpoint, retryCount + 1); } if (error.response?.status === 404) { return null; // Resource not found } throw new Error(`VirusTotal API error: ${error.response?.status || error.message}`); } } private async enforceRateLimit(): Promise<void> { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequest; // Free tier: max 4 requests per minute // Premium tier: higher limits const minInterval = this.vtConfig.tier === 'premium' ? 100 : 15000; // 15s for free tier if (timeSinceLastRequest < minInterval) { const waitTime = minInterval - timeSinceLastRequest; await this.wait(waitTime); } } private wait(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } private getCachedResult(key: string): any | null { const cached = this.cache.get(key); if (cached && Date.now() < cached.expires) { return cached.data; } if (cached) { this.cache.delete(key); } return null; } private setCachedResult(key: string, data: any, ttl = 3600000): void { // 1 hour default this.cache.set(key, { data, expires: Date.now() + ttl }); } /** * Fetch threat intelligence data (main interface method) * Uses recent malicious samples from various sources */ public async fetchThreatData(): Promise<ThreatFeedResult> { // VirusTotal doesn't have a "recent threats" endpoint, so we'll analyze // some known malicious hashes and URLs for demonstration const sampleMaliciousHashes = [ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', // Known malware sample '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', // Sample hash '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' // Another sample ]; const indicators: ThreatIndicator[] = []; // Analyze each hash and collect results for (const hash of sampleMaliciousHashes) { try { const result = await this.analyzeHash(hash); indicators.push(...result.indicators); } catch (error) { console.warn(`Failed to analyze hash ${hash}:`, error); } } return { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalCount: indicators.length, totalIndicators: indicators.length, requestsProcessed: sampleMaliciousHashes.length } }; } /** * Analyze a file hash */ public async analyzeHash(hash: string): Promise<ThreatFeedResult> { const cacheKey = `hash:${hash}`; const cached = this.getCachedResult(cacheKey); if (cached) { return cached; } try { const response: VirusTotalFileResponse = await this.makeRequest(`/files/${hash}`); if (!response) { return { source: this.config.name, timestamp: new Date(), indicators: [], metadata: { totalCount: 0 } }; } const indicators = this.parseFileResponse(response); const result = { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalCount: indicators.length, analysisDate: new Date(response.data.attributes.last_analysis_date * 1000), detectionStats: response.data.attributes.last_analysis_stats, reputation: response.data.attributes.reputation, fileSize: response.data.attributes.size, engines: Object.keys(response.data.attributes.last_analysis_results).length } }; this.setCachedResult(cacheKey, result); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`VirusTotal file analysis failed: ${errorMessage}`); } } /** * Analyze a URL */ public async analyzeUrl(url: string): Promise<ThreatFeedResult> { const cacheKey = `url:${url}`; const cached = this.getCachedResult(cacheKey); if (cached) { return cached; } try { // First submit URL for analysis const urlId = Buffer.from(url).toString('base64').replace(/=/g, ''); let response: VirusTotalUrlResponse = await this.makeRequest(`/urls/${urlId}`); if (!response) { // Submit URL for scanning await this.submitUrl(url); // Wait a bit for analysis to complete await this.wait(5000); response = await this.makeRequest(`/urls/${urlId}`); } if (!response) { return { source: this.config.name, timestamp: new Date(), indicators: [], metadata: { totalCount: 0 } }; } const indicators = this.parseUrlResponse(response, url); const result = { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalCount: indicators.length, analysisDate: new Date(response.data.attributes.last_analysis_date * 1000), detectionStats: response.data.attributes.last_analysis_stats, reputation: response.data.attributes.reputation, votes: response.data.attributes.total_votes, engines: Object.keys(response.data.attributes.last_analysis_results).length } }; this.setCachedResult(cacheKey, result); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`VirusTotal URL analysis failed: ${errorMessage}`); } } /** * Analyze a domain */ public async analyzeDomain(domain: string): Promise<ThreatFeedResult> { const cacheKey = `domain:${domain}`; const cached = this.getCachedResult(cacheKey); if (cached) { return cached; } try { const response: VirusTotalDomainResponse = await this.makeRequest(`/domains/${domain}`); if (!response) { return { source: this.config.name, timestamp: new Date(), indicators: [], metadata: { totalCount: 0 } }; } const indicators = this.parseDomainResponse(response, domain); const result = { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalCount: indicators.length, analysisDate: new Date(response.data.attributes.last_analysis_date * 1000), detectionStats: response.data.attributes.last_analysis_stats, reputation: response.data.attributes.reputation, categories: response.data.attributes.categories, dnsRecords: response.data.attributes.last_dns_records } }; this.setCachedResult(cacheKey, result); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`VirusTotal domain analysis failed: ${errorMessage}`); } } private async submitUrl(url: string): Promise<void> { try { await axios.post(`${this.vtConfig.baseUrl}/urls`, `url=${encodeURIComponent(url)}`, { headers: { 'x-apikey': this.vtConfig.apiKey, 'Content-Type': 'application/x-www-form-urlencoded' } } ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn(`[VirusTotal] Failed to submit URL for analysis: ${errorMessage}`); } } private parseFileResponse(response: VirusTotalFileResponse): ThreatIndicator[] { const attrs = response.data.attributes; const indicators: ThreatIndicator[] = []; // Determine severity based on detection ratio const totalEngines = Object.keys(attrs.last_analysis_results).length; const maliciousDetections = attrs.last_analysis_stats.malicious; const detectionRatio = maliciousDetections / totalEngines; let severity: ThreatIndicator['severity']; if (detectionRatio >= 0.7) { severity = 'critical'; } else if (detectionRatio >= 0.4) { severity = 'high'; } else if (detectionRatio >= 0.1) { severity = 'medium'; } else { severity = 'low'; } // Create indicators for each hash type const hashTypes = ['sha256', 'md5', 'sha1'] as const; hashTypes.forEach(hashType => { if (attrs[hashType]) { indicators.push({ type: 'hash', value: attrs[hashType], confidence: Math.min(detectionRatio + 0.3, 1.0), firstSeen: new Date(attrs.last_analysis_date * 1000), lastSeen: new Date(), source: this.config.name, tags: this.extractTags(attrs.last_analysis_results, attrs.popular_threat_classification), malwareFamily: attrs.popular_threat_classification?.suggested_threat_label, severity, metadata: { hashType, detectionRatio, maliciousDetections, totalEngines, fileSize: attrs.size, reputation: attrs.reputation } }); } }); return indicators; } private parseUrlResponse(response: VirusTotalUrlResponse, originalUrl: string): ThreatIndicator[] { const attrs = response.data.attributes; const indicators: ThreatIndicator[] = []; const totalEngines = Object.keys(attrs.last_analysis_results).length; const maliciousDetections = attrs.last_analysis_stats.malicious; const detectionRatio = maliciousDetections / totalEngines; let severity: ThreatIndicator['severity']; if (detectionRatio >= 0.5) { severity = 'critical'; } else if (detectionRatio >= 0.3) { severity = 'high'; } else if (detectionRatio >= 0.1) { severity = 'medium'; } else { severity = 'low'; } // URL indicator indicators.push({ type: 'url', value: originalUrl, confidence: Math.min(detectionRatio + 0.2, 1.0), firstSeen: new Date(attrs.last_analysis_date * 1000), lastSeen: new Date(), source: this.config.name, tags: this.extractUrlTags(attrs.last_analysis_results), severity, metadata: { detectionRatio, maliciousDetections, totalEngines, reputation: attrs.reputation, votes: attrs.total_votes } }); // Extract domain indicator try { const url = new URL(originalUrl); indicators.push({ type: 'domain', value: url.hostname, confidence: Math.min(detectionRatio + 0.1, 1.0), firstSeen: new Date(attrs.last_analysis_date * 1000), lastSeen: new Date(), source: this.config.name, tags: ['url-associated', ...this.extractUrlTags(attrs.last_analysis_results)], severity, metadata: { associatedUrl: originalUrl, detectionRatio, reputation: attrs.reputation } }); } catch (e) { // Invalid URL format } return indicators; } private parseDomainResponse(response: VirusTotalDomainResponse, domain: string): ThreatIndicator[] { const attrs = response.data.attributes; const indicators: ThreatIndicator[] = []; const totalEngines = Object.keys(attrs.last_analysis_stats).length; const maliciousDetections = attrs.last_analysis_stats.malicious; const detectionRatio = totalEngines > 0 ? maliciousDetections / totalEngines : 0; let severity: ThreatIndicator['severity']; if (detectionRatio >= 0.5) { severity = 'critical'; } else if (detectionRatio >= 0.3) { severity = 'high'; } else if (detectionRatio >= 0.1) { severity = 'medium'; } else { severity = 'low'; } // Domain indicator indicators.push({ type: 'domain', value: domain, confidence: Math.min(detectionRatio + 0.2, 1.0), firstSeen: new Date(attrs.last_analysis_date * 1000), lastSeen: new Date(), source: this.config.name, tags: this.extractDomainTags(attrs.categories), severity, metadata: { detectionRatio, maliciousDetections, reputation: attrs.reputation, categories: attrs.categories, dnsRecordCount: attrs.last_dns_records?.length || 0 } }); // Extract IP indicators from DNS records attrs.last_dns_records?.forEach(record => { if (record.type === 'A' || record.type === 'AAAA') { indicators.push({ type: 'ip', value: record.value, confidence: Math.min(detectionRatio, 0.8), firstSeen: new Date(attrs.last_analysis_date * 1000), lastSeen: new Date(), source: this.config.name, tags: ['dns-resolution', domain], severity, metadata: { recordType: record.type, ttl: record.ttl, associatedDomain: domain } }); } }); return indicators; } private extractTags(analysisResults: Record<string, any>, threatClassification?: any): string[] { const tags = new Set<string>(); // Add common malware family tags Object.values(analysisResults).forEach((result: any) => { if (result.result && result.result !== 'Clean') { const resultLower = result.result.toLowerCase(); // Common malware types if (resultLower.includes('trojan')) { tags.add('trojan'); } if (resultLower.includes('virus')) { tags.add('virus'); } if (resultLower.includes('worm')) { tags.add('worm'); } if (resultLower.includes('adware')) { tags.add('adware'); } if (resultLower.includes('spyware')) { tags.add('spyware'); } if (resultLower.includes('ransomware')) { tags.add('ransomware'); } if (resultLower.includes('backdoor')) { tags.add('backdoor'); } if (resultLower.includes('rootkit')) { tags.add('rootkit'); } if (resultLower.includes('downloader')) { tags.add('downloader'); } if (resultLower.includes('banker')) { tags.add('banking'); } } }); // Add threat classification tags if (threatClassification?.popular_threat_category) { threatClassification.popular_threat_category.forEach((cat: any) => { tags.add(cat.value.toLowerCase().replace(/\s+/g, '-')); }); } tags.add('virustotal'); return Array.from(tags); } private extractUrlTags(analysisResults: Record<string, any>): string[] { const tags = new Set(['url-scan', 'virustotal']); Object.values(analysisResults).forEach((result: any) => { if (result.result && result.result !== 'clean') { const resultLower = result.result.toLowerCase(); if (resultLower.includes('phishing')) { tags.add('phishing'); } if (resultLower.includes('malware')) { tags.add('malware'); } if (resultLower.includes('suspicious')) { tags.add('suspicious'); } if (resultLower.includes('scam')) { tags.add('scam'); } if (resultLower.includes('fraud')) { tags.add('fraud'); } } }); return Array.from(tags); } private extractDomainTags(categories: Record<string, string>): string[] { const tags = new Set(['domain-scan', 'virustotal']); Object.values(categories).forEach(category => { const categoryLower = category.toLowerCase(); tags.add(categoryLower.replace(/\s+/g, '-')); }); return Array.from(tags); } /** * Get feed statistics */ public getStats(): { requestCount: number; errorCount: number; lastRequest: Date | null; cacheSize: number; cacheHitRatio: number; rateLimit: any; } { return { requestCount: this.requestCount, errorCount: this.errorCount, lastRequest: this.lastRequest ? new Date(this.lastRequest) : null, cacheSize: this.cache.size, cacheHitRatio: this.requestCount > 0 ? (this.requestCount - this.errorCount) / this.requestCount : 0, rateLimit: this.config.rateLimit }; } /** * Update configuration */ public updateConfig(updates: Partial<VirusTotalConfig>): void { this.vtConfig = { ...this.vtConfig, ...updates }; if (updates.apiKey) { this.config.apiKey = updates.apiKey; this.validateApiKey(); } } /** * Clear cache */ public clearCache(): void { this.cache.clear(); } }