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.

480 lines (423 loc) 13.2 kB
/** * AlienVault OTX Feed Integration * * Provides secure integration with AlienVault Open Threat Exchange (OTX) * - Fetches subscribed pulses with threat indicators * - Supports optional API key authentication for higher rate limits * - Converts OTX pulse data to standardized ThreatIndicator format * - Implements secure rate limiting and error handling */ import axios, { AxiosInstance } from 'axios'; import { ThreatIndicator, ThreatFeedResult, FeedConfiguration, TrojanHorseError, RateLimitError } from '../types'; export interface OTXPulse { id: string; name: string; description: string; author_name: string; public: boolean; created: string; modified: string; TLP: 'white' | 'green' | 'amber' | 'red'; tags: string[]; targeted_countries: string[]; adversary: string; indicators: OTXIndicator[]; } export interface OTXIndicator { id: number; indicator: string; type: string; title: string; description: string; created: string; is_active: boolean | number; access_type: 'public' | 'private' | 'redacted'; content: string; role: string | null; expiration: string | null; observations: number; } export interface OTXResponse { count: number; next: string | null; previous: string | null; results: OTXPulse[]; } export class AlienVaultFeed { private config: FeedConfiguration; private httpClient: AxiosInstance; private lastFetch: Date | null = null; private rateLimit: { requestsPerHour: number; requestCount: number; resetTime: Date; }; constructor(config: Partial<FeedConfiguration> = {}) { this.config = { name: 'AlienVault OTX', endpoint: 'https://otx.alienvault.com/api/v1/pulses/subscribed', rateLimit: { requestsPerHour: config.apiKey ? 1000 : 100, // Higher with API key burstLimit: 5, retryAfter: 5000 }, timeout: 30000, retries: 3, ...config }; // Initialize rate limiting this.rateLimit = { requestsPerHour: this.config.rateLimit?.requestsPerHour || 100, requestCount: 0, resetTime: new Date(Date.now() + 60 * 60 * 1000) // 1 hour from now }; // Configure HTTP client with security headers this.httpClient = axios.create({ baseURL: 'https://otx.alienvault.com/api/v1', timeout: this.config.timeout || 30000, headers: { 'User-Agent': 'TrojanHorse.js/1.0.1 (Security Research)', 'Accept': 'application/json', 'Content-Type': 'application/json', ...(this.config.apiKey && { 'X-OTX-API-KEY': this.config.apiKey }) }, validateStatus: (status) => status < 500, // Allow client errors for handling maxRedirects: 5 // Remove httpsAgent configuration that's causing issues }); // Set up response interceptors for rate limiting this.httpClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 429) { const retryAfter = parseInt(error.response.headers['retry-after'] || '60'); throw new RateLimitError( 'AlienVault OTX rate limit exceeded', retryAfter * 1000, { provider: 'AlienVault OTX', endpoint: error.config?.url, resetTime: new Date(Date.now() + retryAfter * 1000) } ); } return Promise.reject(error); } ); } /** * Fetch threat intelligence data (main interface method) */ public async fetchThreatData(): Promise<ThreatFeedResult> { return this.fetchThreats({ modifiedSince: this.lastFetch || new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours limit: 100, minimumConfidence: 0.5 }); } /** * Fetch threat intelligence from AlienVault OTX subscribed pulses */ public async fetchThreats(options: { modifiedSince?: Date; limit?: number; minimumConfidence?: number; } = {}): Promise<ThreatFeedResult> { try { await this.checkRateLimit(); const { modifiedSince, limit = 100, minimumConfidence = 0.5 } = options; // Build query parameters const params: Record<string, string> = { limit: limit.toString(), page: '1' }; if (modifiedSince) { params.modified_since = modifiedSince.toISOString(); } const response = await this.httpClient.get<OTXResponse>('/pulses/subscribed', { params }); if (response.status !== 200) { throw new TrojanHorseError( `AlienVault OTX API returned status ${response.status}`, 'FEED_API_ERROR', response.status ); } const otxData = response.data; const indicators: ThreatIndicator[] = []; // Process each pulse and its indicators for (const pulse of otxData.results) { for (const otxIndicator of pulse.indicators) { // Skip inactive indicators if (!otxIndicator.is_active) { continue; } // Convert OTX indicator to standardized format const indicator = this.convertOTXIndicator(otxIndicator, pulse, minimumConfidence); if (indicator) { indicators.push(indicator); } } } this.lastFetch = new Date(); return { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalPulses: otxData.count, totalIndicators: indicators.length, hasMore: !!otxData.next, nextPage: otxData.next, rateLimit: { remaining: this.rateLimit.requestsPerHour - this.rateLimit.requestCount, resetTime: this.rateLimit.resetTime, limit: this.rateLimit.requestsPerHour } } }; } catch (error) { if (error instanceof RateLimitError || error instanceof TrojanHorseError) { throw error; } throw new TrojanHorseError( `Failed to fetch from AlienVault OTX: ${error instanceof Error ? error.message : 'Unknown error'}`, 'FEED_FETCH_FAILED', 500, { originalError: error instanceof Error ? error.message : String(error), provider: 'AlienVault OTX' } ); } } /** * Convert OTX indicator to standardized ThreatIndicator format */ private convertOTXIndicator( otxIndicator: OTXIndicator, pulse: OTXPulse, minimumConfidence: number ): ThreatIndicator | null { try { // Map OTX indicator types to our standard types const typeMapping: Record<string, ThreatIndicator['type']> = { 'IPv4': 'ip', 'IPv6': 'ip', 'domain': 'domain', 'hostname': 'domain', 'URL': 'url', 'email': 'email', 'FileHash-MD5': 'hash', 'FileHash-SHA1': 'hash', 'FileHash-SHA256': 'hash', 'FileHash-PEHASH': 'hash', 'FileHash-IMPHASH': 'hash', 'FilePath': 'file_path' }; const indicatorType = typeMapping[otxIndicator.type]; if (!indicatorType) { // Skip unsupported indicator types return null; } // Calculate confidence based on OTX factors const confidence = this.calculateConfidence(otxIndicator, pulse); if (confidence < minimumConfidence) { return null; } // Determine severity based on TLP and pulse metadata const severity = this.determineSeverity(pulse, otxIndicator); return { type: indicatorType, value: otxIndicator.indicator, confidence, firstSeen: new Date(otxIndicator.created), lastSeen: new Date(pulse.modified), source: `AlienVault OTX - ${pulse.author_name}`, tags: [ ...pulse.tags, ...(otxIndicator.role ? [otxIndicator.role] : []), ...(pulse.adversary ? [pulse.adversary] : []) ].filter(Boolean), malwareFamily: this.extractMalwareFamily(pulse), severity }; } catch (error) { // Log parsing error but don't fail the entire operation console.warn(`Failed to parse OTX indicator ${otxIndicator.id}:`, error); return null; } } /** * Calculate confidence score based on OTX indicator quality signals */ private calculateConfidence(indicator: OTXIndicator, pulse: OTXPulse): number { let confidence = 0.5; // Base confidence // Boost confidence for active indicators if (indicator.is_active) { confidence += 0.1; } // Boost for public access (more validated) if (indicator.access_type === 'public') { confidence += 0.1; } // Boost for observations/votes if (indicator.observations > 0) { confidence += Math.min(indicator.observations * 0.05, 0.2); } // Boost for detailed description if (indicator.description && indicator.description.length > 10) { confidence += 0.05; } // Boost for recent indicators const daysSinceCreated = (Date.now() - new Date(indicator.created).getTime()) / (1000 * 60 * 60 * 24); if (daysSinceCreated < 7) { confidence += 0.1; } else if (daysSinceCreated < 30) { confidence += 0.05; } // TLP-based confidence adjustment const tlpBoost = { 'red': 0.3, 'amber': 0.2, 'green': 0.1, 'white': 0.05 }; confidence += tlpBoost[pulse.TLP] || 0; return Math.min(confidence, 1.0); } /** * Determine severity based on pulse and indicator metadata */ private determineSeverity(pulse: OTXPulse, indicator: OTXIndicator): ThreatIndicator['severity'] { // High severity for red TLP if (pulse.TLP === 'red') { return 'critical'; } if (pulse.TLP === 'amber') { return 'high'; } // Check for high-risk roles const highRiskRoles = [ 'command_and_control', 'malware_hosting', 'exploit_kit', 'ransomware', 'trojan', 'backdoor' ]; if (indicator.role && highRiskRoles.includes(indicator.role)) { return 'high'; } // Check for medium-risk roles const mediumRiskRoles = [ 'phishing', 'bruteforce', 'web_attack', 'scanning_host' ]; if (indicator.role && mediumRiskRoles.includes(indicator.role)) { return 'medium'; } // Default based on activity and observations if (indicator.observations > 10) { return 'medium'; } return 'low'; } /** * Extract malware family from pulse metadata */ private extractMalwareFamily(pulse: OTXPulse): string | undefined { // Check pulse name for common malware families const malwareFamilies = [ 'emotet', 'trickbot', 'dridex', 'qakbot', 'cobalt strike', 'ransomware', 'trojan', 'backdoor', 'rootkit', 'worm' ]; const pulseName = pulse.name.toLowerCase(); const malwareFamily = malwareFamilies.find(family => pulseName.includes(family.toLowerCase()) ); return malwareFamily || pulse.adversary || undefined; } /** * Check rate limiting before making requests */ private async checkRateLimit(): Promise<void> { const now = new Date(); // Reset rate limit counter if an hour has passed if (now > this.rateLimit.resetTime) { this.rateLimit.requestCount = 0; this.rateLimit.resetTime = new Date(now.getTime() + 60 * 60 * 1000); } // Check if we've exceeded the rate limit if (this.rateLimit.requestCount >= this.rateLimit.requestsPerHour) { const resetIn = this.rateLimit.resetTime.getTime() - now.getTime(); throw new RateLimitError( 'AlienVault OTX rate limit exceeded', resetIn, { provider: 'AlienVault OTX', requestsPerHour: this.rateLimit.requestsPerHour, resetTime: this.rateLimit.resetTime } ); } this.rateLimit.requestCount++; } /** * Test the feed connection and API key validity */ public async testConnection(): Promise<boolean> { try { // Test with a simple API call const response = await this.httpClient.get('/pulses/subscribed', { params: { limit: '1' } }); return response.status === 200; } catch (error) { return false; } } /** * Get feed statistics and health information */ public getStats(): { lastFetch: Date | null; nextAllowedFetch: Date; rateLimit: FeedConfiguration['rateLimit']; hasApiKey: boolean; } { const nextAllowedFetch = new Date( Math.max( Date.now() + (this.config.rateLimit?.retryAfter || 5000), this.rateLimit.resetTime.getTime() ) ); return { lastFetch: this.lastFetch, nextAllowedFetch, rateLimit: this.config.rateLimit!, hasApiKey: !!this.config.apiKey }; } /** * Get configuration information */ public getConfig(): FeedConfiguration { // Return config without exposing sensitive API key return { ...this.config, apiKey: this.config.apiKey ? '***masked***' : '' }; } }