import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { logger } from '../logger.js'; import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js'; import { LRUCache } from 'lru-cache'; /** * Reputation check result information */ export interface IReputationResult { score: number; // 0 (worst) to 100 (best) isSpam: boolean; // true if the IP is known for spam isProxy: boolean; // true if the IP is a known proxy isTor: boolean; // true if the IP is a known Tor exit node isVPN: boolean; // true if the IP is a known VPN country?: string; // Country code (if available) asn?: string; // Autonomous System Number (if available) org?: string; // Organization name (if available) blacklists?: string[]; // Names of blacklists that include this IP timestamp: number; // When this result was created/retrieved error?: string; // Error message if check failed } /** * Reputation threshold scores */ export enum ReputationThreshold { HIGH_RISK = 20, // Score below this is considered high risk MEDIUM_RISK = 50, // Score below this is considered medium risk LOW_RISK = 80 // Score below this is considered low risk (but not trusted) } /** * IP type classifications */ export enum IPType { RESIDENTIAL = 'residential', DATACENTER = 'datacenter', PROXY = 'proxy', TOR = 'tor', VPN = 'vpn', UNKNOWN = 'unknown' } /** * Options for the IP Reputation Checker */ export interface IIPReputationOptions { maxCacheSize?: number; // Maximum number of IPs to cache (default: 10000) cacheTTL?: number; // TTL for cache entries in ms (default: 24 hours) dnsblServers?: string[]; // List of DNSBL servers to check highRiskThreshold?: number; // Score below this is high risk mediumRiskThreshold?: number; // Score below this is medium risk lowRiskThreshold?: number; // Score below this is low risk enableLocalCache?: boolean; // Whether to persist cache to disk (default: true) enableDNSBL?: boolean; // Whether to use DNSBL checks (default: true) enableIPInfo?: boolean; // Whether to use IP info service (default: true) } /** * Class for checking IP reputation of inbound email senders */ export class IPReputationChecker { private static instance: IPReputationChecker; private reputationCache: LRUCache; private options: Required; // Default DNSBL servers private static readonly DEFAULT_DNSBL_SERVERS = [ 'zen.spamhaus.org', // Spamhaus 'bl.spamcop.net', // SpamCop 'b.barracudacentral.org', // Barracuda 'spam.dnsbl.sorbs.net', // SORBS 'dnsbl.sorbs.net', // SORBS (expanded) 'cbl.abuseat.org', // Composite Blocking List 'xbl.spamhaus.org', // Spamhaus XBL 'pbl.spamhaus.org', // Spamhaus PBL 'dnsbl-1.uceprotect.net', // UCEPROTECT 'psbl.surriel.com' // PSBL ]; // Default options private static readonly DEFAULT_OPTIONS: Required = { maxCacheSize: 10000, cacheTTL: 24 * 60 * 60 * 1000, // 24 hours dnsblServers: IPReputationChecker.DEFAULT_DNSBL_SERVERS, highRiskThreshold: ReputationThreshold.HIGH_RISK, mediumRiskThreshold: ReputationThreshold.MEDIUM_RISK, lowRiskThreshold: ReputationThreshold.LOW_RISK, enableLocalCache: true, enableDNSBL: true, enableIPInfo: true }; /** * Constructor for IPReputationChecker * @param options Configuration options */ constructor(options: IIPReputationOptions = {}) { // Merge with default options this.options = { ...IPReputationChecker.DEFAULT_OPTIONS, ...options }; // Initialize reputation cache this.reputationCache = new LRUCache({ max: this.options.maxCacheSize, ttl: this.options.cacheTTL, // Cache TTL }); // Load cache from disk if enabled if (this.options.enableLocalCache) { this.loadCache(); } } /** * Get the singleton instance of the checker * @param options Configuration options * @returns Singleton instance */ public static getInstance(options: IIPReputationOptions = {}): IPReputationChecker { if (!IPReputationChecker.instance) { IPReputationChecker.instance = new IPReputationChecker(options); } return IPReputationChecker.instance; } /** * Check an IP address's reputation * @param ip IP address to check * @returns Reputation check result */ public async checkReputation(ip: string): Promise { try { // Validate IP address format if (!this.isValidIPAddress(ip)) { logger.log('warn', `Invalid IP address format: ${ip}`); return this.createErrorResult(ip, 'Invalid IP address format'); } // Check cache first const cachedResult = this.reputationCache.get(ip); if (cachedResult) { logger.log('info', `Using cached reputation data for IP ${ip}`, { score: cachedResult.score, isSpam: cachedResult.isSpam }); return cachedResult; } // Initialize empty result const result: IReputationResult = { score: 100, // Start with perfect score isSpam: false, isProxy: false, isTor: false, isVPN: false, timestamp: Date.now() }; // Check IP against DNS blacklists if enabled if (this.options.enableDNSBL) { const dnsblResult = await this.checkDNSBL(ip); // Update result with DNSBL information result.score -= dnsblResult.listCount * 10; // Subtract 10 points per blacklist result.isSpam = dnsblResult.listCount > 0; result.blacklists = dnsblResult.lists; } // Get additional IP information if enabled if (this.options.enableIPInfo) { const ipInfo = await this.getIPInfo(ip); // Update result with IP info result.country = ipInfo.country; result.asn = ipInfo.asn; result.org = ipInfo.org; // Adjust score based on IP type if (ipInfo.type === IPType.PROXY || ipInfo.type === IPType.TOR || ipInfo.type === IPType.VPN) { result.score -= 30; // Subtract 30 points for proxies, Tor, VPNs // Set proxy flags result.isProxy = ipInfo.type === IPType.PROXY; result.isTor = ipInfo.type === IPType.TOR; result.isVPN = ipInfo.type === IPType.VPN; } } // Ensure score is between 0 and 100 result.score = Math.max(0, Math.min(100, result.score)); // Update cache with result this.reputationCache.set(ip, result); // Save cache if enabled if (this.options.enableLocalCache) { this.saveCache(); } // Log the reputation check this.logReputationCheck(ip, result); return result; } catch (error) { logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, { ip, stack: error.stack }); return this.createErrorResult(ip, error.message); } } /** * Check an IP against DNS blacklists * @param ip IP address to check * @returns DNSBL check results */ private async checkDNSBL(ip: string): Promise<{ listCount: number; lists: string[]; }> { try { // Reverse the IP for DNSBL queries const reversedIP = this.reverseIP(ip); const results = await Promise.allSettled( this.options.dnsblServers.map(async (server) => { try { const lookupDomain = `${reversedIP}.${server}`; await plugins.dns.promises.resolve(lookupDomain); return server; // IP is listed in this DNSBL } catch (error) { if (error.code === 'ENOTFOUND') { return null; // IP is not listed in this DNSBL } throw error; // Other error } }) ); // Extract successful lookups (listed in DNSBL) const lists = results .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled' && result.value !== null ) .map(result => result.value); return { listCount: lists.length, lists }; } catch (error) { logger.log('error', `Error checking DNSBL for ${ip}: ${error.message}`); return { listCount: 0, lists: [] }; } } /** * Get information about an IP address * @param ip IP address to check * @returns IP information */ private async getIPInfo(ip: string): Promise<{ country?: string; asn?: string; org?: string; type: IPType; }> { try { // In a real implementation, this would use an IP data service API // For this implementation, we'll use a simplified approach // Check if it's a known Tor exit node (simplified) const isTor = ip.startsWith('171.25.') || ip.startsWith('185.220.') || ip.startsWith('95.216.'); // Check if it's a known VPN (simplified) const isVPN = ip.startsWith('185.156.') || ip.startsWith('37.120.'); // Check if it's a known proxy (simplified) const isProxy = ip.startsWith('34.92.') || ip.startsWith('34.206.'); // Determine IP type let type = IPType.UNKNOWN; if (isTor) { type = IPType.TOR; } else if (isVPN) { type = IPType.VPN; } else if (isProxy) { type = IPType.PROXY; } else { // Simple datacenters detection (major cloud providers) if ( ip.startsWith('13.') || // AWS ip.startsWith('35.') || // Google Cloud ip.startsWith('52.') || // AWS ip.startsWith('34.') || // Google Cloud ip.startsWith('104.') // Various providers ) { type = IPType.DATACENTER; } else { type = IPType.RESIDENTIAL; } } // Return the information return { country: this.determineCountry(ip), // Simplified, would use geolocation service asn: 'AS12345', // Simplified, would look up real ASN org: this.determineOrg(ip), // Simplified, would use real org data type }; } catch (error) { logger.log('error', `Error getting IP info for ${ip}: ${error.message}`); return { type: IPType.UNKNOWN }; } } /** * Simplified method to determine country from IP * In a real implementation, this would use a geolocation database or service * @param ip IP address * @returns Country code */ private determineCountry(ip: string): string { // Simplified mapping for demo purposes if (ip.startsWith('13.') || ip.startsWith('52.')) return 'US'; if (ip.startsWith('35.') || ip.startsWith('34.')) return 'US'; if (ip.startsWith('185.')) return 'NL'; if (ip.startsWith('171.')) return 'DE'; return 'XX'; // Unknown } /** * Simplified method to determine organization from IP * In a real implementation, this would use an IP-to-org database or service * @param ip IP address * @returns Organization name */ private determineOrg(ip: string): string { // Simplified mapping for demo purposes if (ip.startsWith('13.') || ip.startsWith('52.')) return 'Amazon AWS'; if (ip.startsWith('35.') || ip.startsWith('34.')) return 'Google Cloud'; if (ip.startsWith('185.156.')) return 'NordVPN'; if (ip.startsWith('37.120.')) return 'ExpressVPN'; if (ip.startsWith('185.220.')) return 'Tor Exit Node'; return 'Unknown'; } /** * Reverse an IP address for DNSBL lookups (e.g., 1.2.3.4 -> 4.3.2.1) * @param ip IP address to reverse * @returns Reversed IP for DNSBL queries */ private reverseIP(ip: string): string { return ip.split('.').reverse().join('.'); } /** * Create an error result for when reputation check fails * @param ip IP address * @param errorMessage Error message * @returns Error result */ private createErrorResult(ip: string, errorMessage: string): IReputationResult { return { score: 50, // Neutral score for errors isSpam: false, isProxy: false, isTor: false, isVPN: false, timestamp: Date.now(), error: errorMessage }; } /** * Validate IP address format * @param ip IP address to validate * @returns Whether the IP is valid */ private isValidIPAddress(ip: string): boolean { // IPv4 regex pattern const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; return ipv4Pattern.test(ip); } /** * Log reputation check to security logger * @param ip IP address * @param result Reputation result */ private logReputationCheck(ip: string, result: IReputationResult): void { // Determine log level based on reputation score let logLevel = SecurityLogLevel.INFO; if (result.score < this.options.highRiskThreshold) { logLevel = SecurityLogLevel.WARN; } else if (result.score < this.options.mediumRiskThreshold) { logLevel = SecurityLogLevel.INFO; } // Log the check SecurityLogger.getInstance().logEvent({ level: logLevel, type: SecurityEventType.IP_REPUTATION, message: `IP reputation check ${result.isSpam ? 'flagged spam' : 'completed'} for ${ip}`, ipAddress: ip, details: { score: result.score, isSpam: result.isSpam, isProxy: result.isProxy, isTor: result.isTor, isVPN: result.isVPN, country: result.country, blacklists: result.blacklists }, success: !result.isSpam }); } /** * Save cache to disk */ private saveCache(): void { try { // Convert cache entries to serializable array const entries = Array.from(this.reputationCache.entries()).map(([ip, data]) => ({ ip, data })); // Only save if we have entries if (entries.length === 0) { return; } // Ensure directory exists const cacheDir = plugins.path.join(paths.dataDir, 'security'); plugins.smartfile.fs.ensureDirSync(cacheDir); // Save to file const cacheFile = plugins.path.join(cacheDir, 'ip_reputation_cache.json'); plugins.smartfile.memory.toFsSync( JSON.stringify(entries), cacheFile ); logger.log('info', `Saved ${entries.length} IP reputation cache entries to disk`); } catch (error) { logger.log('error', `Failed to save IP reputation cache: ${error.message}`); } } /** * Load cache from disk */ private loadCache(): void { try { const cacheFile = plugins.path.join(paths.dataDir, 'security', 'ip_reputation_cache.json'); // Check if file exists if (!plugins.fs.existsSync(cacheFile)) { return; } // Read and parse cache const cacheData = plugins.fs.readFileSync(cacheFile, 'utf8'); const entries = JSON.parse(cacheData); // Validate and filter entries const now = Date.now(); const validEntries = entries.filter(entry => { const age = now - entry.data.timestamp; return age < this.options.cacheTTL; // Only load entries that haven't expired }); // Restore cache for (const entry of validEntries) { this.reputationCache.set(entry.ip, entry.data); } logger.log('info', `Loaded ${validEntries.length} IP reputation cache entries from disk`); } catch (error) { logger.log('error', `Failed to load IP reputation cache: ${error.message}`); } } /** * Get the risk level for a reputation score * @param score Reputation score (0-100) * @returns Risk level description */ public static getRiskLevel(score: number): 'high' | 'medium' | 'low' | 'trusted' { if (score < ReputationThreshold.HIGH_RISK) { return 'high'; } else if (score < ReputationThreshold.MEDIUM_RISK) { return 'medium'; } else if (score < ReputationThreshold.LOW_RISK) { return 'low'; } else { return 'trusted'; } } }