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.

636 lines (568 loc) 18.8 kB
/** * AbuseIPDB Feed Integration * * Integrates with AbuseIPDB's IP reputation and abuse database * - IP reputation scoring and confidence ratings * - Abuse category classifications * - Country and ISP information * - Free tier: 1000 requests/day, Premium: higher limits */ import axios, { AxiosResponse } from 'axios'; import { FeedConfiguration, ThreatIndicator, ThreatFeedResult } from '../types'; // AbuseIPDB API response interfaces interface AbuseIPDBResponse { data: { ipAddress: string; isPublic: boolean; ipVersion: number; isWhitelisted: boolean; abuseConfidencePercentage: number; countryCode: string | null; countryName: string | null; usageType: string; isp: string | null; domain: string | null; hostnames: string[]; totalReports: number; numDistinctUsers: number; lastReportedAt: string | null; }; } // interface AbuseIPDBBulkResponse { // data: Array<{ // ipAddress: string; // abuseConfidencePercentage: number; // lastReportedAt: string; // }>; // } interface AbuseIPDBReportsResponse { data: { ipAddress: string; reports: Array<{ reportedAt: string; comment: string; categories: number[]; reporterId: number; reporterCountryCode: string; reporterCountryName: string; }>; }; } interface AbuseIPDBError { errors: Array<{ detail: string; status: number; source?: { parameter?: string; }; }>; } // AbuseIPDB abuse categories mapping const ABUSE_CATEGORIES = { 1: 'DNS Compromise', 2: 'DNS Poisoning', 3: 'Fraud Orders', 4: 'DDoS Attack', 5: 'FTP Brute-Force', 6: 'Ping of Death', 7: 'Phishing', 8: 'Fraud VoIP', 9: 'Open Proxy', 10: 'Web Spam', 11: 'Email Spam', 12: 'Blog Spam', 13: 'VPN IP', 14: 'Port Scan', 15: 'Hacking', 16: 'SQL Injection', 17: 'Spoofing', 18: 'Brute-Force', 19: 'Bad Web Bot', 20: 'Exploited Host', 21: 'Web App Attack', 22: 'SSH', 23: 'IoT Targeted' } as const; export class AbuseIPDBFeed { private config: FeedConfiguration; private apiKey: string; private baseUrl: string = 'https://api.abuseipdb.com/api/v2'; private rateLimitRemaining: number = 1000; private rateLimitReset: Date = new Date(); private requestCount: number = 0; private dailyLimit: number = 1000; // Free tier default constructor(config: Partial<FeedConfiguration> = {}) { this.config = { name: 'AbuseIPDB', type: 'api', endpoint: 'https://api.abuseipdb.com/api/v2', authentication: { type: 'api_key', required: true, header: 'Key', credentials: {} }, rateLimit: { requestsPerHour: 1000, // Conservative for free tier burstLimit: 5, retryAfter: 1000 }, enabled: true, priority: 'high', sslPinning: true, timeout: 30000, retries: 3, ...config }; this.apiKey = config.apiKey || process.env.ABUSEIPDB_API_KEY || ''; if (!this.apiKey) { throw new Error('AbuseIPDB API key is required. Set ABUSEIPDB_API_KEY environment variable or provide apiKey in config.'); } // Detect premium tier based on rate limits // Premium users typically have higher limits this.dailyLimit = 1000; // Default to free tier } /** * Check a single IP address for abuse reports */ public async checkIP(ip: string, options: { maxAgeInDays?: number; verbose?: boolean; } = {}): Promise<AbuseIPDBResponse> { const { maxAgeInDays = 90, verbose = false } = options; await this.checkRateLimit(); try { const params = new URLSearchParams({ ipAddress: ip, maxAgeInDays: maxAgeInDays.toString(), verbose: verbose.toString() }); const response: AxiosResponse<AbuseIPDBResponse> = await axios.get( `${this.baseUrl}/check`, { headers: { 'Key': this.apiKey, 'Accept': 'application/json' }, params, timeout: this.config.timeout || 30000 } ); this.updateRateLimitInfo(response.headers); this.requestCount++; return response.data; } catch (error: any) { if (error.response?.status === 429) { const retryAfter = parseInt(error.response.headers['retry-after'] || '60'); throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds`); } if (error.response?.status === 422) { const abuseError = error.response.data as AbuseIPDBError; const detail = abuseError.errors?.[0]?.detail || 'Invalid parameter'; throw new Error(`AbuseIPDB validation error: ${detail}`); } if (error.response?.status === 401) { throw new Error('Invalid AbuseIPDB API key'); } throw new Error(`AbuseIPDB API error: ${error.message}`); } } /** * Get detailed reports for an IP address */ public async getReports(ip: string, options: { maxAgeInDays?: number; perPage?: number; page?: number; } = {}): Promise<AbuseIPDBReportsResponse> { const { maxAgeInDays = 90, perPage = 25, page = 1 } = options; await this.checkRateLimit(); try { const params = new URLSearchParams({ ipAddress: ip, maxAgeInDays: maxAgeInDays.toString(), perPage: perPage.toString(), page: page.toString() }); const response: AxiosResponse<AbuseIPDBReportsResponse> = await axios.get( `${this.baseUrl}/reports`, { headers: { 'Key': this.apiKey, 'Accept': 'application/json' }, params, timeout: this.config.timeout || 30000 } ); this.updateRateLimitInfo(response.headers); this.requestCount++; return response.data; } catch (error: any) { if (error.response?.status === 429) { const retryAfter = parseInt(error.response.headers['retry-after'] || '60'); throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds`); } throw new Error(`AbuseIPDB reports API error: ${error.message}`); } } /** * Fetch threat intelligence data (main interface method) * Since AbuseIPDB requires specific IPs, this uses a sample of known bad IPs */ public async fetchThreatData(): Promise<ThreatFeedResult> { // Use a sample of commonly reported malicious IPs for demonstration // Production implementation uses enterprise IP lists and customer-specific indicators const sampleIPs = [ '185.220.101.32', // Tor exit node (often flagged) '192.42.116.16', // Known scanner '194.147.102.87', // Known malicious IP '45.148.10.62', // VPN/proxy commonly flagged '89.248.174.241' // Scanner/bot ]; return this.fetchThreats({ ips: sampleIPs, maxAgeInDays: 30, confidenceThreshold: 50, includeReports: false, maxResults: 50 }); } /** * Fetch threat data and convert to standard format */ public async fetchThreats(options: { ips?: string[]; maxAgeInDays?: number; confidenceThreshold?: number; includeReports?: boolean; maxResults?: number; } = {}): Promise<ThreatFeedResult> { const { ips = [], maxAgeInDays = 90, confidenceThreshold = 25, includeReports = false, maxResults = 100 } = options; const indicators: ThreatIndicator[] = []; const errors: string[] = []; // AbuseIPDB requires specific IP addresses to check if (ips.length === 0) { console.warn('AbuseIPDB requires specific IP addresses to check'); return { source: this.config.name, timestamp: new Date(), indicators: [], metadata: { totalCount: 0, totalIndicators: 0, hasMore: false, errors: ['No IP addresses provided for AbuseIPDB lookup'] } }; } // Process each IP for (const ip of ips.slice(0, maxResults)) { try { const abuseData = await this.checkIP(ip, { maxAgeInDays, verbose: true }); // Only include IPs that meet confidence threshold if (abuseData.data.abuseConfidencePercentage >= confidenceThreshold) { let reports: AbuseIPDBReportsResponse['data']['reports'] | undefined = undefined; // Optionally fetch detailed reports if (includeReports && abuseData.data.totalReports > 0) { try { const reportsData = await this.getReports(ip, { maxAgeInDays }); reports = reportsData.data.reports; } catch (error: any) { console.warn(`Failed to fetch reports for ${ip}:`, error.message); } } const indicator = this.mapToThreatIndicator(ip, abuseData.data, reports); if (indicator) { indicators.push(indicator); } } } catch (error: any) { errors.push(`${ip}: ${error.message}`); console.warn(`Failed to fetch AbuseIPDB data for ${ip}:`, error.message); } // Respect rate limits if (this.rateLimitRemaining <= 1) { console.warn('AbuseIPDB rate limit approaching, stopping batch processing'); break; } } return { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalCount: indicators.length, totalIndicators: indicators.length, hasMore: false, requestsProcessed: ips.length, confidenceThreshold, errors: errors.length > 0 ? errors : [], rateLimit: { remaining: this.rateLimitRemaining, resetTime: this.rateLimitReset, limit: 1000 // Default daily limit for free tier } } }; } /** * Map AbuseIPDB data to our ThreatIndicator format */ private mapToThreatIndicator( ip: string, data: AbuseIPDBResponse['data'], reports?: AbuseIPDBReportsResponse['data']['reports'] ): ThreatIndicator | null { // Skip if confidence is too low or whitelisted if (data.abuseConfidencePercentage < 25 || data.isWhitelisted) { return null; } // Extract abuse categories from reports const abuseCategories = new Set<number>(); if (reports) { reports.forEach(report => { report.categories.forEach(cat => abuseCategories.add(cat)); }); } const confidence = this.calculateConfidence(data, reports); const severity = this.mapSeverity(data.abuseConfidencePercentage); return { type: 'ip', value: ip, confidence, severity, firstSeen: reports && reports.length > 0 ? new Date(Math.min(...reports.map(r => new Date(r.reportedAt).getTime()))) : new Date(), lastSeen: data.lastReportedAt ? new Date(data.lastReportedAt) : new Date(), source: this.config.name, tags: [ ...Array.from(abuseCategories).map(cat => ABUSE_CATEGORIES[cat as keyof typeof ABUSE_CATEGORIES] || `category-${cat}` ), data.usageType, 'abuseipdb' ].filter(Boolean), metadata: { abuseipdb: { abuseConfidencePercentage: data.abuseConfidencePercentage, totalReports: data.totalReports, numDistinctUsers: data.numDistinctUsers, isPublic: data.isPublic, isWhitelisted: data.isWhitelisted, ipVersion: data.ipVersion, country: { code: data.countryCode, name: data.countryName }, usageType: data.usageType, isp: data.isp, domain: data.domain, hostnames: data.hostnames, categories: Array.from(abuseCategories), reports: reports?.slice(0, 10)?.map(report => ({ reportedAt: report.reportedAt, comment: report.comment?.substring(0, 200), // Truncate long comments categories: report.categories, reporterCountry: report.reporterCountryName })) } }, description: this.generateDescription(data, Array.from(abuseCategories)), malwareFamily: this.extractMalwareFamily(Array.from(abuseCategories)) }; } /** * Calculate confidence score based on AbuseIPDB data */ private calculateConfidence( data: AbuseIPDBResponse['data'], reports?: AbuseIPDBReportsResponse['data']['reports'] ): number { let confidence = data.abuseConfidencePercentage / 100; // Base on AbuseIPDB confidence // Adjust based on report volume if (data.totalReports > 50) { confidence += 0.1; } else if (data.totalReports > 10) { confidence += 0.05; } // Adjust based on distinct reporters if (data.numDistinctUsers > 10) { confidence += 0.1; } else if (data.numDistinctUsers > 5) { confidence += 0.05; } // Recent activity increases confidence if (data.lastReportedAt) { const daysSinceLastReport = (Date.now() - new Date(data.lastReportedAt).getTime()) / (1000 * 60 * 60 * 24); if (daysSinceLastReport < 7) { confidence += 0.1; } else if (daysSinceLastReport < 30) { confidence += 0.05; } } // Multiple distinct abuse categories increase confidence if (reports) { const categories = new Set(); reports.forEach(r => r.categories.forEach(c => categories.add(c))); if (categories.size > 3) { confidence += 0.1; } else if (categories.size > 1) { confidence += 0.05; } } return Math.max(0.1, Math.min(confidence, 1.0)); } /** * Map AbuseIPDB confidence percentage to severity levels */ private mapSeverity(confidencePercentage: number): 'low' | 'medium' | 'high' | 'critical' { if (confidencePercentage >= 75) { return 'critical'; } if (confidencePercentage >= 50) { return 'high'; } if (confidencePercentage >= 25) { return 'medium'; } return 'low'; } /** * Generate human-readable description */ private generateDescription( data: AbuseIPDBResponse['data'], categories: number[] ): string { const parts: string[] = []; parts.push(`IP reported with ${data.abuseConfidencePercentage}% confidence`); if (data.totalReports > 0) { parts.push(`${data.totalReports} reports from ${data.numDistinctUsers} users`); } if (categories.length > 0) { const categoryNames = categories .map(cat => ABUSE_CATEGORIES[cat as keyof typeof ABUSE_CATEGORIES]) .filter(Boolean) .slice(0, 3); if (categoryNames.length > 0) { parts.push(`Categories: ${categoryNames.join(', ')}`); } } if (data.countryName) { parts.push(`Location: ${data.countryName}`); } return parts.join('. '); } /** * Extract potential malware family from abuse categories */ private extractMalwareFamily(categories: number[]): string | undefined { // Map certain categories to malware families for (const cat of categories) { switch (cat) { case 4: return 'DDoS Botnet'; case 15: return 'Hacking Tools'; case 19: return 'Malicious Bot'; case 20: return 'Compromised Host'; case 21: return 'Web Attack Tools'; case 23: return 'IoT Malware'; } } return undefined; } /** * Check rate limit before making requests */ private async checkRateLimit(): Promise<void> { const now = new Date(); // Reset daily counter if needed if (now.getTime() - this.rateLimitReset.getTime() > 24 * 60 * 60 * 1000) { this.requestCount = 0; this.rateLimitReset = new Date(now.getTime() + 24 * 60 * 60 * 1000); } // Check daily limit if (this.requestCount >= this.dailyLimit) { const resetTime = this.rateLimitReset.getTime() - now.getTime(); throw new Error(`Daily rate limit exceeded. Resets in ${Math.ceil(resetTime / 1000 / 60)} minutes`); } // Check burst limit if (this.rateLimitRemaining <= 0) { throw new Error('Rate limit exceeded. Please wait before making more requests'); } } /** * Update rate limit information from response headers */ private updateRateLimitInfo(headers: any): void { const remaining = headers['x-ratelimit-remaining']; const reset = headers['x-ratelimit-reset']; if (remaining !== undefined) { this.rateLimitRemaining = parseInt(remaining); } if (reset !== undefined) { this.rateLimitReset = new Date(parseInt(reset) * 1000); } } /** * Get current statistics */ public getStats(): { requestCount: number; rateLimitRemaining: number; rateLimitReset: Date; dailyLimit: number; isEnabled: boolean; hasApiKey: boolean; rateLimit: FeedConfiguration['rateLimit']; } { return { requestCount: this.requestCount, rateLimitRemaining: this.rateLimitRemaining, rateLimitReset: this.rateLimitReset, dailyLimit: this.dailyLimit, isEnabled: this.config.enabled || false, hasApiKey: !!this.apiKey, rateLimit: this.config.rateLimit }; } /** * Test API connectivity and authentication */ public async testConnection(): Promise<{ success: boolean; message: string; details?: any }> { try { // Test with a known malicious IP (if available) or a safe test IP const testIP = '127.0.0.1'; // localhost - should be safe to test await this.checkIP(testIP, { maxAgeInDays: 30 }); return { success: true, message: 'AbuseIPDB API connection successful', details: { endpoint: this.baseUrl, rateLimitRemaining: this.rateLimitRemaining, dailyLimit: this.dailyLimit } }; } catch (error: any) { return { success: false, message: `AbuseIPDB API connection failed: ${error.message}`, details: { hasApiKey: !!this.apiKey, endpoint: this.baseUrl, error: error.message } }; } } }