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 threshold scores */ export var ReputationThreshold; (function (ReputationThreshold) { ReputationThreshold[ReputationThreshold["HIGH_RISK"] = 20] = "HIGH_RISK"; ReputationThreshold[ReputationThreshold["MEDIUM_RISK"] = 50] = "MEDIUM_RISK"; ReputationThreshold[ReputationThreshold["LOW_RISK"] = 80] = "LOW_RISK"; // Score below this is considered low risk (but not trusted) })(ReputationThreshold || (ReputationThreshold = {})); /** * IP type classifications */ export var IPType; (function (IPType) { IPType["RESIDENTIAL"] = "residential"; IPType["DATACENTER"] = "datacenter"; IPType["PROXY"] = "proxy"; IPType["TOR"] = "tor"; IPType["VPN"] = "vpn"; IPType["UNKNOWN"] = "unknown"; })(IPType || (IPType = {})); /** * Class for checking IP reputation of inbound email senders */ export class IPReputationChecker { static instance; reputationCache; options; storageManager; // StorageManager instance // Default DNSBL servers static 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 static DEFAULT_OPTIONS = { 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 * @param storageManager Optional StorageManager instance for persistence */ constructor(options = {}, storageManager) { // Merge with default options this.options = { ...IPReputationChecker.DEFAULT_OPTIONS, ...options }; this.storageManager = storageManager; // If no storage manager provided, log warning if (!storageManager && this.options.enableLocalCache) { logger.log('warn', '⚠️ WARNING: IPReputationChecker initialized without StorageManager.\n' + ' IP reputation cache will only be stored to filesystem.\n' + ' Consider passing a StorageManager instance for better storage flexibility.'); } // 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) { // Fire and forget the load operation this.loadCache().catch(error => { logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`); }); } } /** * Get the singleton instance of the checker * @param options Configuration options * @param storageManager Optional StorageManager instance for persistence * @returns Singleton instance */ static getInstance(options = {}, storageManager) { if (!IPReputationChecker.instance) { IPReputationChecker.instance = new IPReputationChecker(options, storageManager); } return IPReputationChecker.instance; } /** * Check an IP address's reputation * @param ip IP address to check * @returns Reputation check result */ async checkReputation(ip) { 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 = { 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) { // Fire and forget the save operation this.saveCache().catch(error => { logger.log('error', `Failed to save IP reputation cache: ${error.message}`); }); } // 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 */ async checkDNSBL(ip) { 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.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 */ async getIPInfo(ip) { 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 */ determineCountry(ip) { // 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 */ determineOrg(ip) { // 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 */ reverseIP(ip) { 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 */ createErrorResult(ip, errorMessage) { 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 */ isValidIPAddress(ip) { // 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 */ logReputationCheck(ip, result) { // 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 or storage manager */ async saveCache() { 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; } const cacheData = JSON.stringify(entries); // Save to storage manager if available if (this.storageManager) { await this.storageManager.set('/security/ip-reputation-cache.json', cacheData); logger.log('info', `Saved ${entries.length} IP reputation cache entries to StorageManager`); } else { // Fall back to filesystem const cacheDir = plugins.path.join(paths.dataDir, 'security'); await plugins.smartfs.directory(cacheDir).recursive().create(); const cacheFile = plugins.path.join(cacheDir, 'ip_reputation_cache.json'); await plugins.smartfs.file(cacheFile).write(cacheData); 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 or storage manager */ async loadCache() { try { let cacheData = null; let fromFilesystem = false; // Try to load from storage manager first if (this.storageManager) { try { cacheData = await this.storageManager.get('/security/ip-reputation-cache.json'); if (!cacheData) { // Check if data exists in filesystem and migrate it const cacheFile = plugins.path.join(paths.dataDir, 'security', 'ip_reputation_cache.json'); if (plugins.fs.existsSync(cacheFile)) { logger.log('info', 'Migrating IP reputation cache from filesystem to StorageManager'); cacheData = plugins.fs.readFileSync(cacheFile, 'utf8'); fromFilesystem = true; // Migrate to storage manager await this.storageManager.set('/security/ip-reputation-cache.json', cacheData); logger.log('info', 'IP reputation cache migrated to StorageManager successfully'); // Optionally delete the old file after successful migration try { plugins.fs.unlinkSync(cacheFile); logger.log('info', 'Old cache file removed after migration'); } catch (deleteError) { logger.log('warn', `Could not delete old cache file: ${deleteError.message}`); } } } } catch (error) { logger.log('error', `Error loading from StorageManager: ${error.message}`); } } else { // No storage manager, load from filesystem const cacheFile = plugins.path.join(paths.dataDir, 'security', 'ip_reputation_cache.json'); if (plugins.fs.existsSync(cacheFile)) { cacheData = plugins.fs.readFileSync(cacheFile, 'utf8'); fromFilesystem = true; } } // Parse and restore cache if data was found if (cacheData) { 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); } const source = fromFilesystem ? 'disk' : 'StorageManager'; logger.log('info', `Loaded ${validEntries.length} IP reputation cache entries from ${source}`); } } 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 */ static getRiskLevel(score) { 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'; } } /** * Update the storage manager after instantiation * This is useful when the storage manager is not available at construction time * @param storageManager The StorageManager instance to use */ updateStorageManager(storageManager) { this.storageManager = storageManager; logger.log('info', 'IPReputationChecker storage manager updated'); // If cache is enabled and we have entries, save them to the new storage manager if (this.options.enableLocalCache && this.reputationCache.size > 0) { this.saveCache().catch(error => { logger.log('error', `Failed to save cache to new storage manager: ${error.message}`); }); } } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5pcHJlcHV0YXRpb25jaGVja2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvc2VjdXJpdHkvY2xhc3Nlcy5pcHJlcHV0YXRpb25jaGVja2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sS0FBSyxLQUFLLE1BQU0sYUFBYSxDQUFDO0FBQ3JDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDdEMsT0FBTyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2xHLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFtQnJDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksbUJBSVg7QUFKRCxXQUFZLG1CQUFtQjtJQUM3Qix3RUFBYyxDQUFBO0lBQ2QsNEVBQWdCLENBQUE7SUFDaEIsc0VBQWEsQ0FBQSxDQUFRLDREQUE0RDtBQUNuRixDQUFDLEVBSlcsbUJBQW1CLEtBQW5CLG1CQUFtQixRQUk5QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksTUFPWDtBQVBELFdBQVksTUFBTTtJQUNoQixxQ0FBMkIsQ0FBQTtJQUMzQixtQ0FBeUIsQ0FBQTtJQUN6Qix5QkFBZSxDQUFBO0lBQ2YscUJBQVcsQ0FBQTtJQUNYLHFCQUFXLENBQUE7SUFDWCw2QkFBbUIsQ0FBQTtBQUNyQixDQUFDLEVBUFcsTUFBTSxLQUFOLE1BQU0sUUFPakI7QUFpQkQ7O0dBRUc7QUFDSCxNQUFNLE9BQU8sbUJBQW1CO0lBQ3RCLE1BQU0sQ0FBQyxRQUFRLENBQXNCO0lBQ3JDLGVBQWUsQ0FBc0M7SUFDckQsT0FBTyxDQUFpQztJQUN4QyxjQUFjLENBQU8sQ0FBQywwQkFBMEI7SUFFeEQsd0JBQXdCO0lBQ2hCLE1BQU0sQ0FBVSxxQkFBcUIsR0FBRztRQUM5QyxrQkFB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