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.

643 lines (573 loc) 18.8 kB
/** * CrowdSec CTI Feed Integration * * Integrates with CrowdSec's Cyber Threat Intelligence API * - Behavior-based threat detection * - IP reputation and malicious activity analysis * - Scenarios: http_crawl, ssh_bruteforce, port_scan, etc. * - Free tier: 1000 requests/day, Premium: unlimited */ import axios, { AxiosResponse } from 'axios'; import { FeedConfiguration, ThreatIndicator, ThreatFeedResult } from '../types'; // CrowdSec API response interfaces interface CrowdSecIPInfo { ip: string; ip_range?: string; ip_range_score?: number; country?: string; city?: string; latitude?: number; longitude?: number; as_name?: string; as_num?: number; background_noise_score?: number; scores?: { overall?: { aggressiveness: number; threat: number; trust: number; anomaly: number; total: number; }; }; attack_details?: Array<{ name: string; label: string; description: string; references?: string[]; }>; behaviors?: Array<{ name: string; label: string; description: string; }>; history?: { first_seen: string; last_seen: string; full_age: number; days_age: number; }; classifications?: { false_positives?: Array<{ name: string; label: string; description: string; }>; classifications?: Array<{ name: string; label: string; description: string; }>; }; target_countries?: Record<string, number>; background_noise?: boolean; cves?: string[]; } interface CrowdSecSmokeResponse { ip_range_score: number; ip: string; ip_range: string; as_name: string; as_num: number; location: { country: string; city: string; latitude: number; longitude: number; }; reverse_dns: string; behaviors: Array<{ name: string; label: string; description: string; }>; history: { first_seen: string; last_seen: string; full_age: number; days_age: number; }; classifications: { false_positives: any[]; classifications: Array<{ name: string; label: string; description: string; }>; }; attack_details: Array<{ name: string; label: string; description: string; references: string[]; }>; target_countries: Record<string, number>; background_noise: boolean; scores: { overall: { aggressiveness: number; threat: number; trust: number; anomaly: number; total: number; }; }; } // interface CrowdSecError { // message: string; // errors?: string[]; // } export class CrowdSecFeed { private config: FeedConfiguration; private apiKey: string; private baseUrl: string = 'https://cti-api.crowdsec.net/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: 'CrowdSec CTI', type: 'api', endpoint: 'https://cti-api.crowdsec.net/v2', authentication: { type: 'api_key', required: true, header: 'x-api-key', credentials: {} }, rateLimit: { requestsPerHour: 1000, // Conservative for free tier burstLimit: 10, retryAfter: 1000 }, enabled: true, priority: 'high', sslPinning: true, timeout: 30000, retries: 3, ...config }; this.apiKey = config.apiKey || process.env.CROWDSEC_API_KEY || ''; if (!this.apiKey) { console.warn('CrowdSec API key not provided. Some features may be limited.'); } // Set daily limit based on tier if (this.apiKey) { // TODO: Detect premium tier from API response headers this.dailyLimit = 1000; // Default to free tier } } /** * Fetch threat intelligence data (main interface method) * Uses a curated list of known malicious IPs and recent threat data */ public async fetchThreatData(): Promise<ThreatFeedResult> { // Since CrowdSec CTI is primarily for querying specific IPs, // we'll use a smoke API or community blocklist to get general threats return this.fetchRecentThreats(); } /** * Fetch recent threat intelligence using CrowdSec's smoke endpoint * This provides a sample of recent malicious activity */ public async fetchRecentThreats(): Promise<ThreatFeedResult> { try { const response = await axios.get(`${this.baseUrl}/smoke/`, { headers: { 'User-Agent': 'TrojanHorse.js/1.0.1', 'Accept': 'application/json', ...(this.apiKey && { 'x-api-key': this.apiKey }) }, timeout: this.config.timeout || 30000 }); const smokeData: CrowdSecSmokeResponse[] = Array.isArray(response.data) ? response.data : [response.data]; const indicators: ThreatIndicator[] = []; for (const entry of smokeData) { const indicator = this.mapToThreatIndicator(entry.ip, entry); if (indicator) { indicators.push(indicator); } } return { source: this.config.name, timestamp: new Date(), indicators, metadata: { totalCount: indicators.length, totalIndicators: indicators.length, hasMore: false, requestsProcessed: 1 } }; } catch (error: any) { throw new Error(`Failed to fetch CrowdSec smoke data: ${error.message}`); } } /** * Fetch threat intelligence for a specific IP address */ public async fetchIPInfo(ip: string): Promise<CrowdSecIPInfo> { if (!this.apiKey) { throw new Error('CrowdSec API key is required for IP information'); } await this.checkRateLimit(); try { const response: AxiosResponse<CrowdSecIPInfo> = await axios.get( `${this.baseUrl}/cti/${ip}`, { headers: { 'x-api-key': this.apiKey, 'User-Agent': 'TrojanHorse.js/1.0.1', 'Accept': 'application/json' }, 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 === 404) { // IP not found in CrowdSec database - not necessarily an error throw new Error(`IP ${ip} not found in CrowdSec database`); } if (error.response?.status === 401) { throw new Error('Invalid CrowdSec API key'); } throw new Error(`CrowdSec API error: ${error.message}`); } } /** * Fetch smoke data (lightweight IP check) */ public async fetchSmokeData(ip: string): Promise<CrowdSecSmokeResponse> { await this.checkRateLimit(); try { const url = this.apiKey ? `${this.baseUrl}/smoke/${ip}` : `${this.baseUrl}/smoke/${ip}`; const headers: Record<string, string> = { 'User-Agent': 'TrojanHorse.js/1.0.1', 'Accept': 'application/json' }; if (this.apiKey) { headers['x-api-key'] = this.apiKey; } const response: AxiosResponse<CrowdSecSmokeResponse> = await axios.get(url, { headers, 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 === 404) { throw new Error(`IP ${ip} not found in CrowdSec smoke database`); } throw new Error(`CrowdSec Smoke API error: ${error.message}`); } } /** * Fetch threat data and convert to standard format */ public async fetchThreats(options: { ips?: string[]; includeSmoke?: boolean; maxResults?: number; } = {}): Promise<ThreatFeedResult> { const { ips = [], includeSmoke = true, maxResults = 100 } = options; const indicators: ThreatIndicator[] = []; const errors: string[] = []; // If no specific IPs provided, we can't fetch random threats from CrowdSec // This is different from URLhaus which provides a feed if (ips.length === 0) { console.warn('CrowdSec 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 CrowdSec lookup'] } }; } // Process each IP for (const ip of ips.slice(0, maxResults)) { try { let crowdSecData: CrowdSecIPInfo | CrowdSecSmokeResponse; if (this.apiKey && !includeSmoke) { crowdSecData = await this.fetchIPInfo(ip); } else { crowdSecData = await this.fetchSmokeData(ip); } const indicator = this.mapToThreatIndicator(ip, crowdSecData); if (indicator) { indicators.push(indicator); } } catch (error: any) { errors.push(`${ip}: ${error.message}`); console.warn(`Failed to fetch CrowdSec data for ${ip}:`, error.message); } // Respect rate limits if (this.rateLimitRemaining <= 1) { console.warn('CrowdSec 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, errors: errors.length > 0 ? errors : [], rateLimit: { remaining: this.rateLimitRemaining, resetTime: this.rateLimitReset, limit: 1000 // Default daily limit for free tier } } }; } /** * Map CrowdSec data to our ThreatIndicator format */ private mapToThreatIndicator( ip: string, data: CrowdSecIPInfo | CrowdSecSmokeResponse ): ThreatIndicator | null { // Determine if this IP is malicious based on CrowdSec scores and behaviors const isMalicious = this.calculateThreatLevel(data); if (!isMalicious) { return null; // Skip clean IPs unless explicitly requested } const confidence = this.calculateConfidence(data); const behaviors = data.behaviors || []; const attackDetails = data.attack_details || []; return { type: 'ip', value: ip, confidence, severity: this.mapSeverity(data.scores?.overall?.threat || 0), firstSeen: data.history?.first_seen ? new Date(data.history.first_seen) : new Date(), lastSeen: data.history?.last_seen ? new Date(data.history.last_seen) : new Date(), source: this.config.name, tags: [ ...behaviors.map(b => b.name), ...attackDetails.map(a => a.name), 'crowdsec-cti' ].filter(Boolean), metadata: { crowdsec: { scores: data.scores, country: 'location' in data ? data.location?.country : data.country, city: 'location' in data ? data.location?.city : data.city, asn: data.as_num, asName: data.as_name, behaviors: behaviors.map(b => ({ name: b.name, label: b.label, description: b.description })), attackDetails: attackDetails.map(a => ({ name: a.name, label: a.label, description: a.description, references: a.references })), backgroundNoise: 'background_noise' in data ? data.background_noise : false, ipRange: 'ip_range' in data ? data.ip_range : undefined, ipRangeScore: 'ip_range_score' in data ? data.ip_range_score : undefined } }, description: this.generateDescription(behaviors, attackDetails), malwareFamily: attackDetails.length > 0 ? (attackDetails[0]?.name || 'unknown') : undefined }; } /** * Calculate if IP is malicious based on CrowdSec data */ private calculateThreatLevel(data: CrowdSecIPInfo | CrowdSecSmokeResponse): boolean { const scores = data.scores?.overall; if (!scores) { return false; } // Consider malicious if: // - High threat score (> 3) // - High aggressiveness (> 3) // - Has attack behaviors // - Low trust score (< 2) const hasHighThreat = scores.threat > 3; const hasHighAggression = scores.aggressiveness > 3; const hasAttackBehaviors = (data.attack_details?.length || 0) > 0; const hasLowTrust = scores.trust < 2; const hasOverallHighScore = scores.total > 3; return hasHighThreat || hasHighAggression || hasAttackBehaviors || (hasLowTrust && hasOverallHighScore); } /** * Calculate confidence score (0-1) */ private calculateConfidence(data: CrowdSecIPInfo | CrowdSecSmokeResponse): number { const scores = data.scores?.overall; if (!scores) { return 0.3; // Low confidence without scores } let confidence = 0.5; // Base confidence // Higher threat score increases confidence confidence += (scores.threat / 10) * 0.3; // Multiple attack details increase confidence const attackCount = data.attack_details?.length || 0; confidence += Math.min(attackCount * 0.1, 0.2); // Recent activity increases confidence if (data.history) { const daysOld = data.history.days_age || 0; if (daysOld < 7) { confidence += 0.1; } else if (daysOld < 30) { confidence += 0.05; } } // Background noise decreases confidence if ('background_noise' in data && data.background_noise) { confidence -= 0.2; } // High anomaly score increases confidence confidence += (scores.anomaly / 10) * 0.1; return Math.max(0.1, Math.min(confidence, 1.0)); } /** * Map CrowdSec threat scores to severity levels */ private mapSeverity(threatScore: number): 'low' | 'medium' | 'high' | 'critical' { if (threatScore >= 4) { return 'critical'; } if (threatScore >= 3) { return 'high'; } if (threatScore >= 2) { return 'medium'; } return 'low'; } /** * Generate human-readable description */ private generateDescription( behaviors: Array<{ name: string; label: string; description: string }>, attackDetails: Array<{ name: string; label: string; description: string }> ): string { const parts: string[] = []; if (behaviors.length > 0) { const behaviorNames = behaviors.map(b => b.label || b.name).join(', '); parts.push(`Observed behaviors: ${behaviorNames}`); } if (attackDetails.length > 0) { const attackNames = attackDetails.map(a => a.label || a.name).join(', '); parts.push(`Attack patterns: ${attackNames}`); } return parts.join('. ') || 'Malicious IP identified by CrowdSec CTI'; } /** * 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 IP (Google DNS) const testIP = '8.8.8.8'; if (this.apiKey) { await this.fetchIPInfo(testIP); return { success: true, message: 'CrowdSec CTI API connection successful (authenticated)' }; } else { await this.fetchSmokeData(testIP); return { success: true, message: 'CrowdSec Smoke API connection successful (anonymous)' }; } } catch (error: any) { return { success: false, message: `CrowdSec API connection failed: ${error.message}`, details: { hasApiKey: !!this.apiKey, endpoint: this.baseUrl, error: error.message } }; } } }