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
text/typescript
/**
* 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
}
};
}
}
}